<구글 로그인>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>구글로그인 - [모듈화 사용: type = "module"]</title>
</head>
<body>
<!--
type = text/javascript를 사용하는 경우와 module을 사용하는 경우 호환이 안 되는 부분 발생
ECMAScript6기준으로 작업하는가? WWW CommonJS기준으로 작업하는가? -> 차이있음
-->
<script type="module">
import { firebaseApp } from "../service/firebase.js";
import AuthLogic from "../service/authLogic.js";
console.log(firebaseApp);
const authLogic = new AuthLogic();
console.log(authLogic);
const btnGoogle = document.querySelector("#btnGoogle");
btnGoogle.addEventListener("click", (e) => {
/*
button태그 사용시에 디폴트 타입이 submit이어서 폼 전송이 일어나게되며
이럴 경우 해당 페이지에 리프레쉬 발생함에 따라 이상동작이 발생하므로
반드시 button태그로 click이벤트 처리시에는 주의할 것!
*/
e.preventDefault(); // submit 방지 용으로 추가 할 것!!(이벤트 버블링 방지)
// console.log(e.target + e.currentTarget);
authLogic.login("Google").then((snapshot) => {
console.log(snapshot);
// 구글 서버에서 응답으로 받은 정보를 문자열로 변환함
const temp = JSON.stringify(snapshot);
// 위에서 변환된 정보를 배열로 변환
const jsonDoc = JSON.parse(temp);
// uid - 구글에서 할당되는 아이디값(이게 바뀌면 다른사람임)
// displayName - 구글 계정 이름
// email - 구글 계정 이메일
console.log(jsonDoc.user.displayName);
});
}); // end of btnGoogle
const moveMain = (displayName) => {
console.log("moveMain: " + displayName);
window.location.href = "./index.html?displayName=" + displayName;
};
// 구글 서버에서 상태값의 변화를 체크하고 자동으로 호출해줌
// 개발자가 호출하는 함수가 아님
// 파라미터 user가 바뀌면 자동으로 호출된다는 것
authLogic.onAuthChange((user) => {
// false && 값 -> false가 반환됨
// true && 값 -> 값이 반환됨
// 자바스크립트에서 false인것(false, 0, "", NaN, null, undefined)
// 아래의 user값이 null이라면 false가 반환되지만 값이 있다면 오른쪽의 moveMain메소드가 실행됨
user && moveMain(user.displayName); // 리액트에서 빈번하게 사용
});
function signOut() {
console.log("로그아웃");
// import문 사용시 주의할 것은 반드시 type=module을 추가할 것
// text/javascript와 module 사이에는 호환이 안됨(고려하고 코딩 전개할 것)
// 그럴 경우 유일한 해결 방법은 직업 함수 호출이 안되고
// 태그의 아이디를 사용하여 addEventListener로 함수처리할 것
authLogic.logout();
}
</script>
<button id="btnGoogle">구글로그인</button>
</body>
</html>
<!--
user
:
Ae
accessToken
:
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjVhNTA5ZjAxOWY3MGQ3NzlkODBmMTUyZDFhNWQzMzgxMWFiN2NlZjciLCJ0eXAiOiJKV1QifQ.eyJuYW1lIjoiSnVuZ1l1biBNb2siLCJwaWN0dXJlIjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL2EvQUVkRlRwNjBnTHZQY2VCY0VtTWJhTDRMRnFvZlo3YjVRMFNqeG9ncmFybFc9czk2LWMiLCJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20va2gtMjIxMTI4LTI1MTk0IiwiYXVkIjoia2gtMjIxMTI4LTI1MTk0IiwiYXV0aF90aW1lIjoxNjc1ODE0NTYyLCJ1c2VyX2lkIjoiWmJxTGNhRUV3Z04zMG52OUUxR2phdTYzRUVYMiIsInN1YiI6IlpicUxjYUVFd2dOMzBudjlFMUdqYXU2M0VFWDIiLCJpYXQiOjE2NzU4MTQ1NjIsImV4cCI6MTY3NTgxODE2MiwiZW1haWwiOiJqdW5neXVubW9rQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7Imdvb2dsZS5jb20iOlsiMTA2NjkyNjQ3NzIzMDMzODY5MTA5Il0sImVtYWlsIjpbImp1bmd5dW5tb2tAZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoiZ29vZ2xlLmNvbSJ9fQ.GQ8hev63LtM2icjJgC9f5a4SOevY7etkhIN2gnPPTIOHwlax1Vse60hdR9JCvSo-8BU0edReE5WxImf-I7uxl6PquPV8JRpbTkm4cOcEMvk31A33OS-K6exf-9N4WOJONrul4FV9zJJfZ1Lx6fhfSemyFUlB0XarC5fD_I2oFUxN1logbR9sRF7WjJRuCZdIPQ4Il9DFSR1SHAfOZE13TzozWJ0IQShE__KEdkXFRiOEXuclRJhFJfg_dzz_H2x8V8qzHsNMfWgqUXDtXCmorrmtNwXeXvO4gLdn7T_df0TU64WzCDqKmj-TKwDKga4kg8eIR4F28aZ1_tyepuislg"
auth
:
We {app: FirebaseAppImpl, heartbeatServiceProvider: Provider, config: {…}, currentUser: Ae, emulatorConfig: null, …}
displayName
:
"JungYun Mok"
email
:
"jungyunmok@gmail.com"
emailVerified
:
true
isAnonymous
:
false
metadata
:
Te {createdAt: '1675739490813', lastLoginAt: '1675814562273', lastSignInTime: 'Wed, 08 Feb 2023 00:02:42 GMT', creationTime: 'Tue, 07 Feb 2023 03:11:30 GMT'}
phoneNumber
:
null
photoURL
:
"https://lh3.googleusercontent.com/a/AEdFTp60gLvPceBcEmMbaL4LFqofZ7b5Q0SjxograrlW=s96-c"
proactiveRefresh
:
ye {user: Ae, isRunning: false, timerId: null, errorBackoff: 30000}
providerData
:
[{…}]
providerId
:
"firebase"
reloadListener
:
null
reloadUserInfo
:
{localId: 'ZbqLcaEEwgN30nv9E1Gjau63EEX2', email: 'jungyunmok@gmail.com', displayName: 'JungYun Mok', photoUrl: 'https://lh3.googleusercontent.com/a/AEdFTp60gLvPceBcEmMbaL4LFqofZ7b5Q0SjxograrlW=s96-c', emailVerified: true, …}
stsTokenManager
:
ke {refreshToken: 'APJWN8el0Kp1Sp1jg-Ou-6MJSQjXekrGo7bm4nXWzqUF9eZdc-…UzEKZaptZ_nJmXH5sfRaTDZBUJFAQe3ChHorLxImMpNjqEf3Y', accessToken: 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjVhNTA5ZjAxOWY3MGQ3Nz…T_df0TU64WzCDqKmj-TKwDKga4kg8eIR4F28aZ1_tyepuislg', expirationTime: 1675818164523}
tenantId
:
null
uid
:
"ZbqLcaEEwgN30nv9E1Gjau63EEX2"
-->
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>터짐</title>
</head>
<body>
<script>
window.onload = function () {
// firebaseJSTest1.html에서 로그인 성공하면 index.html 페이지로 이동처리함
// 이 때 쿼리스트링에 snapshot안에 user에 uid, displayName, email 등이 있음
// 여기서 displayName만 꺼내서 전달 받음
// location 객체는 window의 자손객체로 URL에 관한 모든 정보를 관리하는 API임
// 그래서 firebaseJSTest1.html에서 넘겨받는 정보를 알 수 있음 - 매번 달라지니까
const queryString = new URLSearchParams(window.location.search);
// URLSearchParams 객체 생성 후 이 API가 제공하는 get 함수의 파라미터로
// 쿼리스트링의 키값을 넘겨줌 - 대소문자 구분 주의할 것(undefined로 false가 될수도)
// 페이지 흐름에 영향을 주는 인자값임
const displayName = queryString.get("displayName");
console.log("index: " + displayName);
};
</script>
<h2>터짐 메인 페이지</h2>
<!--
button태그 사용시에는 submit 이슈가 있어서
반드시 event.preventDefault() 호츌할 것
-->
<button id="btnLogout">로그아웃</button>
<script type="module">
import { firebaseApp } from "../service/firebase.js";
import AuthLogic from "../service/authLogic.js";
const authLogic = new AuthLogic();
const btnLogout = document.querySelector("#btnLogout");
btnLogout.addEventListener("click", (event) => {
event.preventDefault();
authLogic.logout();
window.location.href = "./firebaseJSTest1.html";
});
</script>
</body>
</html>
<a href = "javascript:함수()"></a>
이 경우 submit에 대한 이슈가 없음
<input type = "button" value = "전송">
이 경우도 submit에대한 이슈가 없음
<button>전송</button>
submit 이슈가 있음!!(주의할 것)
반드시 event.preventDefault()를 호출하여 이벤트 버블링 방어할 것!!
페이지 새로고침 발생(화면이 하얗게 변하거나 계속 렌더링 됨)
html, css, js는 서버 PC에 물리적인 위치를 갖는다
→ 실행은 로컬 PC에서 실행된다
→ 다운로드됨(서버에서 각 로컬 PC에)
→ O/S(window10)에 깔린 브라우저(V8엔진-구글)가 다운로드된 파일 실행(동기화 안됨)
→ 클라이언트 사이드 렌더링, 정적페이지(이미 결정됨)
→ 다운로드 파일 크기가 크면 속도 오래 걸림
→ 시간을 줄이려면 들여 쓰기, 여백 등 모두 제거해야 함
jquery는 라이브러리이다
jquery.js / jquery.min.js(들여 쓰기나 여백 완전 제거, 사이즈 줄임)
jquery는 표준이 아니다
→ 상속과 확장이 있다(순수한 상태가 아니다, 오염됐다)
→ 다른 이종 간의 것들과 연계(연동)가 어렵다
$ 사용함 → Express language에 사용되는 기호
${아이디|클래스|태그명|document).함수
XXX.html ← document가 가리키는 것 ← write() 함수 쓰면 브라우저에 씀
<div id=d_msg></div>
<!-- 아래 두 코드 같음, text 문자열이라 태그보임 -->
d_msg.innerText
d_msg.text(’값’)
<!-- 아래 두 코드 같음, html이면 태그 안보임 -->
d_msg.innerHTML
d_msg.html(’<h3>값</h3>’)
JSP(Java Server Page)
JSP는 동기화됨
XXX.jsp라고 쓰고 XXX.html이라고 읽는다
마임타입(mime type)으로 무엇인가가 결정됨
image/png, image/jpg, image/gif, video/mp4, video/avi, text/javascript, text/css, text/html, application/msword, application/json
웹+앱 = 하이브리드앱 → 서로 이종 간에 만나는 부분, interface 구현 잘해야 함 → 결합도 낮춤, 통합 가능, 재사용성 높임, 유지보수 편해짐
js - html, js - js, html - jquery - js 전환 잘해야함 → API 많이 보고 순수한(표준적인) 코드를 많이 다룰 것
get
header에 저장된다
입력값이 쿼리스트링으로 전달된다(입력값이 보인다)
링크를 걸 수 있다
전달할 수 있는 값에 제한이 있다
단위테스트 가능하다
브라우저로부터 인터셉트 당함(304 → 200)
post
body에 저장된다
입력값이 보이지 않는다
링크 못 건다
값에 제한 없다
단위테스트 불가하다(자스가 있어야 가능)
포장되어 있어 인터셉트당하지 않는다
<jQuery EasyUI Demo>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Basic DataGrid - jQuery EasyUI Demo</title>
<link
rel="stylesheet"
type="text/css"
href="../../themes/default/easyui.css"
/>
<link rel="stylesheet" type="text/css" href="../themes/icon.css" />
<link rel="stylesheet" type="text/css" href="../demo/demo.css" />
<script type="text/javascript" src="../js/jquery.min.js"></script>
<script type="text/javascript" src="../js/jquery.easyui.min.js"></script>
<script>
getDeptList = () => {
console.log("getDeptList 호출");
$("#dg_dept").datagrid({
// XXX.jsp, XXX.do, XXX.spring
url: "./dept.json",
method: "get",
});
};
</script>
</head>
<body>
<script type="text/javascript">
// ready함수의 파라미터로 함수를 전달할 수 있다
$(document).ready(function () {
console.log("DOM Tree가 그려졌을 때");
});
</script>
<h2>부서관리 시스템</h2>
<div style="margin: 20px 0"></div>
<table
id="dg_dept"
class="easyui-datagrid"
title="부서목록"
style="width: 500px; height: 250px"
toolbar="#toolbar"
data-options="singleSelect:true,collapsible:true"
>
<thead>
<tr>
<th data-options="field:'deptno', width:100">부서번호</th>
<th data-options="field:'dname', width:150">부서명</th>
<th data-options="field:'loc', width:150, align:'center'">지역</th>
</tr>
</thead>
</table>
<div id="toolbar">
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-search"
plain="true"
onclick="getDeptList()"
>조회</a
>
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-edit"
plain="true"
onclick="editUser()"
>Edit User</a
>
<a
href="javascript:void(0)"
class="easyui-linkbutton"
iconCls="icon-remove"
plain="true"
onclick="destroyUser()"
>Remove User</a
>
</div>
</body>
</html>
[
{"deptno":10, "dname":"개발부", "loc":"부산"},
{"deptno":20, "dname":"운영부", "loc":"제주"},
{"deptno":30, "dname":"총무부", "loc":"서울"}
]
<보드판매량>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script type="text/javascript" src="../../js/jquery.min.js"></script>
<link rel="stylesheet" href="./board.css" />
<script type="text/javascript">
// 전역변수 선언자리
const url = "./boardSellAction.html"; // 집계된 보드 판매량 수치
function getBoardSold() {
$.ajax({
type: "GET",
url: url,
// 파라미터 data에는 boardSellAction.html에서 처리된 결과 담음
success: function (data) {
soldProcess(data);
},
});
}
function soldProcess(data) {
console.log("soldProcess 호출 => " + data);
$("#boardSold").html(`<font size="38">${data}</font>`);
const price = $("#price").text();
const cost = $("#cost").text();
let cashPerBoard = price - cost;
let cash = cashPerBoard * data;
$("#cash").html(`<font size="38">${cash}</font>`);
} // end of soldProcess
</script>
</head>
<body>
<!-- type을 생략하면 text/javascript -->
<script>
$(document).ready(function () {
const btnMargin = $("#btnMargin"); // $는 jQuery의미
btnMargin.bind("click", function () {
event.preventDefault();
getBoardSold();
});
});
</script>
<h2>보드 판매량</h2>
<table width="300px" height="80px">
<tr>
<th width="120px">보드 판매량</th>
<td width="180px"><span id="boardSold">10</span></td>
</tr>
<tr>
<th>구매가</th>
<td><span id="cost">100</span>원</td>
</tr>
<tr>
<th>소비자가</th>
<td><span id="price">120</span>원</td>
</tr>
</table>
<h2>마진금액 : <span id="cash">0</span>원</h2>
<button type="submit" id="btnMargin">마진은?</button>
</body>
</html>
<사진 확대 step 1>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<title>Document</title>
<link rel="stylesheet" href="./picture.css?1" />
<script>
const startMethod = (td) => {
// 마우스 오버된 사진의 id값 가져오기
const pic = $(td).attr("id");
console.log("pic값 => " + pic);
$.ajax({
type: "get",
url:
"pictureDetail.html?id=" +
pic +
"timestamp=" +
new Date().getTime(),
data: "id=" + pic,
dataType: "html",
success: function (result) {
$("#d_pic").css("left", $(td).offset().left + 50 + "px");
$("#d_pic").css("top", $(td).offset().top + 50 + "px");
$("#d_pic").html(result);
},
});
};
const clearMethod = () => {
// console.log("clearMethod");
$("#d_pic").html("");
};
</script>
</head>
<body>
<div id="d_pic"></div>
<table border="1">
<thead>
<th colspan="2">그림 목록</th>
</thead>
<tbody>
<tr>
<td align="center">
<img src="../../images/sample/회의.jpg" width="50" height="50" />
</td>
<!-- this는 td -->
<td id="0" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진1
</td>
</tr>
<tr>
<td align="center">
<img src="../../images/sample/회의-1.jpg" width="50" height="50" />
</td>
<td id="1" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진2
</td>
</tr>
<tr>
<td align="center">
<img src="../../images/sample/회의-2.jpg" width="50" height="50" />
</td>
<td id="2" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진3
</td>
</tr>
<tr>
<td align="center">
<img src="../../images/sample/회의-3.jpg" width="50" height="50" />
</td>
<td id="3" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진4
</td>
</tr>
</tbody>
</table>
</body>
</html>
#d_pic {
position: absolute;
top: 200px;
left: 200px;
}
<사진 확대 step 2>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<title>Document</title>
<link rel="stylesheet" href="./picture.css?1" />
<script>
const startMethod = (td) => {
// 마우스 오버된 사진의 id값 가져오기
const pic = $(td).attr("id");
console.log("pic값 => " + pic);
location.href =
"pictureDetail.html?id=" + pic + "×tamp=" + new Date().getTime();
};
const clearMethod = () => {
// console.log("clearMethod");
$("#d_pic").html("");
};
</script>
</head>
<body>
<div id="d_pic"></div>
<table border="1">
<thead>
<th colspan="2">그림 목록</th>
</thead>
<tbody>
<tr>
<td align="center">
<img src="../../images/sample/회의.jpg" width="50" height="50" />
</td>
<!-- this는 td -->
<td id="0" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진1
</td>
</tr>
<tr>
<td align="center">
<img src="../../images/sample/회의-1.jpg" width="50" height="50" />
</td>
<td id="1" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진2
</td>
</tr>
<tr>
<td align="center">
<img src="../../images/sample/회의-2.jpg" width="50" height="50" />
</td>
<td id="2" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진3
</td>
</tr>
<tr>
<td align="center">
<img src="../../images/sample/회의-3.jpg" width="50" height="50" />
</td>
<td id="3" onmouseover="startMethod(this)" onmouseout="clearMethod()">
사진4
</td>
</tr>
</tbody>
</table>
</body>
</html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<script>
$(document).ready(function () {
const queryString = new URLSearchParams(window.location.search);
const id = queryString.get("id");
console.log("pictureDetail값 => " + id);
let img = null;
const pics = ["회의.jpg", "회의-1.jpg", "회의-2.jpg", "회의-3.jpg"];
for (let i = 0; i < pics.length; i++) {
console.log(id + ", " + i);
if (id == i) {
img = pics[i];
console.log(img);
$("#imgDetail").attr("src", "../../images/sample/" + img);
} // end of if
} // end of for
});
</script>
<img
id="imgDetail"
src="../../images/sample/회의.jpg"
width="400px"
height="400px"
alt="그림"
/>
<사진 확대 step 3>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
<title>Document</title>
<link rel="stylesheet" href="./picture.css?1" />
<script>
const startMethod = (td) => {
// 마우스 오버된 사진의 id값 가져오기
const pic = $(td).attr("id");
console.log("pic값 => " + pic);
$.ajax({
type: "get",
url:
"pictureAction.jsp?id=" +
pic +
"×tamp=" +
new Date().getTime(),
data: "id=" + pic,
dataType: "html",
success: function (result) {
$("#d_pic").css("left", $(td).offset().left + 50 + "px");
$("#d_pic").css("top", $(td).offset().top+ "px");
$("#d_pic").html(result);
},
});
};
const clearMethod = () => {
// console.log("clearMethod");
$("#d_pic").html("");
};
</script>
</head>
<body>
<div id="d_pic"></div>
<table border="1">
<thead>
<th colspan="2">그림 목록</th>
</thead>
<tbody>
<tr>
<td align="center"><img src="../../images/sample/회의.jpg"
width="50" height="50" /></td>
<!-- this는 td -->
<td id="0" onmouseover="startMethod(this)"
onmouseout="clearMethod()">사진1</td>
</tr>
<tr>
<td align="center"><img src="../../images/sample/회의-1.jpg"
width="50" height="50" /></td>
<td id="1" onmouseover="startMethod(this)"
onmouseout="clearMethod()">사진2</td>
</tr>
<tr>
<td align="center"><img src="../../images/sample/회의-2.jpg"
width="50" height="50" /></td>
<td id="2" onmouseover="startMethod(this)"
onmouseout="clearMethod()">사진3</td>
</tr>
<tr>
<td align="center"><img src="../../images/sample/회의-3.jpg"
width="50" height="50" /></td>
<td id="3" onmouseover="startMethod(this)"
onmouseout="clearMethod()">사진4</td>
</tr>
</tbody>
</table>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
// 스크립틀릿 = 자바영역
// http://localhost:9000/ajax/picture/b.jsp?id=3
String id = request.getParameter("id");
// out.print(id);
String pics[] = { "회의.jpg", "회의-1.jpg", "회의-2.jpg", "회의-3.jpg" };
String newIMG = null;
for (int i = 0; i < pics.length; i++) {
// out.print(id + ", " + i);
if (Integer.parseInt(id) == i) {
newIMG = pics[i];
} // end of if
} // end of for
%>
<!-- 여기는 html 영역 -->
<img id="imgDetail" src="../../images/sample/<%out.print(newIMG);%>"
width="400px" height="400px" alt="그림" />
'국비학원 > 수업기록' 카테고리의 다른 글
국비 지원 개발자 과정_Day52 (1) | 2023.02.10 |
---|---|
국비 지원 개발자 과정_Day51 (0) | 2023.02.09 |
국비 지원 개발자 과정_Day49 (0) | 2023.02.07 |
국비 지원 개발자 과정_Day48 (0) | 2023.02.04 |
국비 지원 개발자 과정_Day47 (1) | 2023.02.02 |
댓글