75일차
ajax 비동기 요청 보내고 응답받는 방법
fetch xhr
요청에 대한 이야기를 했었다.
49.2 checkbox
checkbox로 값을 보내고 체크된지 아는방법은 checked를 얻어오자.
document.querySelector("#inputMarried2").checked
$("#btn9").click(function() {
const name = $("#inputName2").val();
const email = $("#inputEmail2").val();
const score = $("#inputScore2").val();
const married = document.querySelector("#inputMarried2").checked;
//const married = $("#inputMarried2").is(":checked");
const data = {
name: name,
email: email,
score: score,
married: married
}
$.ajax("/sub36/link2", {
method: "post",
contentType: "application/json",
data: JSON.stringify(data)
});
})50. 응답
ajax요청을 받고 값을 view를 응답하지 않고 값 자체를 응답하는 방법에 대해서 알아보자.
그냥 보내면 404에러가 뜬다.
컨트롤러에게 잘 요청이 갔는데 forward하는 view가 없기때문이다.
리턴타입으로 주로 썻던게 String이다. String은 viewname으로 해석된다.
@ResponseBody ResponseEntity ajax에서 주로봐야하는 것들은 이것들이다.
이것들은 viewName으로 해석되지 않는다.
전체 응답을 명시하는 것이다.
전체응답은 헤더와 body로 나누어져있다. ResponseEntity를 잘 다루면 header와 body를 변환할 수 있다.
51. ResponseEntity
컨트롤러의 리턴타입이 ResponseEntity이면된다.
ResponseEntity객체를 만들어서 리턴해야한다.
created라는 static메소드를 만들거나 생성자를 쓰거나 해야한다.
51.1 생성자
ResponseEntity(HttpStatusCode status)
HttpStatusCode를 파라미터로 받는다. .valueOf() 메소드가 있고 INTERGER를 받는다.
HTTP status Code를 받는다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
100번대 코드는 정보 200번대는 성공적인 응답 300번대는 redircecion관련 응답
400번대는 클라이언트(404에러 없는 자원찾기, 403권한없다.) 500번대는 서버 에러(500에러 예외발생)
이런 코드를 잘쓰면된다.
@GetMapping("link1")
public ResponseEntity method1() {
ResponseEntity res = new ResponseEntity(HttpStatusCode.valueOf(200));
return res;
}Status Code: 200 클라이언트는 입력한 값을 응답받는다.
숫자를 변경할때마다 응답 코드가 나오게된다.
자주쓰는 응답코드들은 ResponseEntity의 static메소드로 이미 있다.
ok() 200 notFound() 404 badRequest() 400 accepted() 202
이런메소드들은 BodyBuilder타입을 리턴한다.
@GetMapping("link2")
public ResponseEntity method2() {
ResponseEntity res = ResponseEntity.ok().build(); //200응답
return res;
}52. ResponseEntity Header- 2
응답 header body로 이루어져있다. 제일중요한것은 body
https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
http응답 메시지는 첫번째줄에 상태라인
두번째 줄부터 헤더들 목록
한칸띄고 세번째줄 body 세부분으로 이루어진 것을 활용하고 있어 메소드들이 이와 관련이 잇다.
주로 관심을 가지고 작성해야하는 부분은 body부분이다.
헤더는 키 : 값으로 이루어져있기 때문에 Map을 만들어서 보내줘야한다.
ResponseEntity(MultiValueMap<String,String> headers, HttpStatusCode status) 다른 생성자를 사용하면된다. 첫번째파라미터는 MultiValueMap 두번째파라미너는 상태코드이다.
MultiValueMap Map타입의 인터페이스이고 HttpHeaders등등을 구현객체로 넣을 수있다.
@GetMapping("link4")
public ResponseEntity method4() {
HttpHeaders header = new HttpHeaders();
header.add("my-header1", "my-value1");
header.add("my-header2", "my-value2");
ResponseEntity res = new ResponseEntity(header, HttpStatusCode.valueOf(200));
return res;
}첫번째줄 상태 다음부터 두번째줄에 header가 들어간 것을 볼 수 있다.
my-header1: my-value1
my-header2: my-value2
52. ResponseEntity body
body를 추가하는 메소드 또는 생성자를 찾아보면
ResponseEntity(T body, HttpStatusCode status)
ResponseEntity(T body, MultiValueMap<String,String> headers, int rawStatus)
ResponseEntity(T body, MultiValueMap<String,String> headers, HttpStatusCode status)메소드는
ok(T body) 200응답코드 body
bodyBuilder등등이 있다.
52.1 생성자
ResponseEntity(T body, HttpStatusCode status)
첫번째 파라미터가 body 두번째 파라미터가 상태코드이다.
타입파라미터에 따라서 body가 결정된다.
@GetMapping("link5")
public ResponseEntity<String> method5() {
ResponseEntity<String> res = new ResponseEntity<String>("hello response", HttpStatusCode.valueOf(200));
return res;
}52.2 메소드
위와 같은 역할을한다. 200요청으로 body에 값이 들어가게된다.
@GetMapping("link6")
public ResponseEntity<String> method6() {
ResponseEntity<String> res = ResponseEntity.ok("hello response2");
return res;
}52.3 연습
7번째 버튼을 클릭하면 응답본문에 현재날짜가 넘어오도록 하기
현재날짜는 LocalDate의 now()메소드를 사용하면된다.
강사님은 toStirng으로 String타입으로 변경해서 보냈다.
@GetMapping("link7")
public ResponseEntity<LocalDate> method7() {
LocalDate now = LocalDate.now();
//String date = LocalDate.now().toString();
ResponseEntity<LocalDate> res = ResponseEntity.ok(now);
return res;
}53. @ResponseBody
꽤많은 경우에 응답도 정상이고 헤더도 세팅할일이 없다면 ResponseEntity를 사용하는게 번거로울 수 있다.
이럴때 사용하는게 @ResponseBody 어노테이션이다.
본문만 리턴하고싶을때 String타입으로 리턴하면 view를 리턴하게 되는데
view로 간주하지말고 본문으로만 리턴하라 할 수 있다.
리턴하는 값 그 자체가 body로 가게 된다.
@GetMapping("link8")
@ResponseBody
public String method8() {
String dateTime = LocalDateTime.now().toString();
return dateTime;
}54. @ResponseBody -2
현재 시간을 응답 하도록 코드를 작성해보자.
LocalTime의 now()메소드에서 현재 시간을 받아올 수 있다.
@GetMapping("link9")
@ResponseBody
public String method9() {
String time = LocalTime.now().toString();
return time;
}55. @ResponseBody JSON
응답 형태를 작성해줘야하는데 주로 JSON형태로 주고받으니 이를 이용해야한다.
JSON을 만들어서 응답 해보자.
JSON형태의 TEXT로 잘오게 된다.
@GetMapping("link10")
@ResponseBody
public String method10() {
String data = """
{"name":"lee", "age":30}
""";
return data;
}그런데 String타입이엇기때문에 JSON인지를 알 수 없다.
그래서 응답할때도 JSON으로 간다는 것을 알려줘야한다.
ResponseEntity객체를 만들어서 보내줘야한다.
@GetMapping("link11")
@ResponseBody
public ResponseEntity<String> method11() {
String data = """
{"name":"lee", "age":30}
""";
HttpHeaders header = new HttpHeaders();
header.set("Content-Type", "application/json");
return new ResponseEntity(data, header, 200);
}56. @ResponseBody JSON - 2
보통은 ajax요청이 JSON으로 주고받으니 HttpHeaders로 헤더를 변경해주는 것이 번거롭다.
이런것도 생략하는 것이 @ResponseBody이다.
리턴타입이 Map, JavaBean이면 알아서 JSON String으로 변환
Content-Type 헤더도 application/json으로 세팅해준다.
@GetMapping("link12")
@ResponseBody()
public Map<String, Object> method12() {
Map<String, Object> map = new HashMap<>();
map.put("name", "박지성");
map.put("age", 44);
return map;
}57 @ResponseBody JSON - 3 연습
13번째 버튼을 클랙햇을때 {"address":"seoul", "price":3.14 }가 리턴되도록하기
@GetMapping("link13")
@ResponseBody()
public Map<String, Object> method13() {
Map<String, Object> map = new HashMap<>();
map.put("address", "seoul");
map.put("price", 3.14);
return map;
}58. @ResponseBody JSON Map- 4
JSON에는 String 숫자 boolean null 객체 배열 등을 넣을 수 있다.
java의 List를 넘기면 json의 배열이 된다.
후에 가면 여기서 들어간 데이터가 DB에서 꺼내진 값이 되는 것이다.
@GetMapping("link14")
@ResponseBody()
public Map<String, Object> method14() {
// Map<String, Object> data = new HashMap<>();
var data = new HashMap<String, Object>();
data.put("name", "차범근");
data.put("score", 8.88);
data.put("married", true);
data.put("position", null);
data.put("child", List.of("차두리", "차하나"));
data.put("food", Map.of("beverage", "coke", "meal", "chicken"));
return data;
}59. @ResponseBody JSON JavaBean- 5
객체가 가지고 있는 프로퍼티들이 json으로 변환된다.
그래서 중요한게 getXXX메소드이다.
스프링은 getXXX메소드를 잘 분석해서 프로퍼티로 리턴된 값을 묶어서 JSON형태로 만든다.
static class Dto1 {
public String getName() {
return "강백호";
}
public Integer getAge() {
return 33;
}
}이런형태의 JavaBean은 {"name" : "강백호", "age":33}이 될 것이다.
59. @ResponseBody JSON JavaBean- 6 연습
{"city":"jeju", "score":3.3, "cap":true }가 전달되도록 JavaBean을 작성해보자.
static class Dto2 {
public String getCity() {
return "jeju";
}
public Double getScore() {
return 3.3;
}
public Boolean isCap() {
return true;
}
public List<String> getFood() {
return List.of("pizza", "chicken", "burger");
}
public Dto1 getSub1() {
return new Dto1();
}
public Map<String, Object> getSub2() {
return Map.of("model", List.of("abc", "def"), "price", 3.14);
}
}@GetMapping("link16")
@ResponseBody
public Dto2 method16() {
return new Dto2(); // {"city":"jeju", "score":3.3, "cap":true}
}60. @ResponseBody JSON JavaBean- 7
복잡한 형태로 보내도 JSON형태로 가게 된다.
@GetMapping("link17")
@ResponseBody
public List<String> method17() {
return List.of("java", "css", "html");
}
@GetMapping("link18")
@ResponseBody
public List<Map<String, Object>> method18() {
return List.of(Map.of("name", "강백호"),
Map.of("name", "채치수"),
Map.of("name", "정대만"));
}
@GetMapping("link19")
@ResponseBody
public List<Dto2> method19() {
return List.of(new Dto2(), new Dto2());
}61. DB @ResponseBody
보낼 객체의 타입에 대해서만 신경쓰면된다.
DB의 mapper를 이용해서 객체를 얻어와보자.
원래는 3tier로 값을 보내고 받아올 것이지만 일단 mapper에서 바로 값을 받아오자.
@Controller
@RequestMapping("sub38")
public class Controller38 {
@Autowired
private Mapper02 mapper02;
@GetMapping("link20")
@ResponseBody
public List<String> mehtod20() {
return mapper02.sql1();
}
}62. DB @ResponseBody - 2
Dto07을 List로 받는 메소드
List라서 배열에 Dto07객체들이 들어가게 된다.
@GetMapping("link21")
@ResponseBody
public List<Dto07> method21() {
return mapper02.sql3();
}63. DB @ResponseBody - 3
mapper의 값으로 파라미터 카테고리id를 받는다.
버튼을 눌럿을때 cid도 같이 보내주고 값을 받아오면된다.
@GetMapping("link22")
@ResponseBody
public List<Dto08> method22(Integer cid) {
return mapper02.sql7(cid);
}<input type="number" id="categoryIdInput" value="1"/>
<button id="btn22">응답 본문 JSON From DB 3 </button>$("#btn22").click(function() {
const cid = $("#categoryIdInput").val();
$.ajax("/sub38/link22?cid=" + cid);
})데이터를 보내고 데이터를 받는 것을 페이지 새로고침(로딩) 없이 실행한 것이다.
Post요청으로 테스트해보았다.
@PostMapping("link22")
@ResponseBody
public List<Dto08> method22(Integer cid) {
return mapper02.sql7(cid);
}$("#btn22").click(function() {
const cid = $("#categoryIdInput").val();
$.ajax("/sub38/link22", {
method: "post",
data: { "cid": cid }
});
})64. DB @ResponseBody - 4
우리가 만드는 컨트롤러에 안에있는 메소드들이 전부다 @ResponseBody어노테이션을 가지고 있다면
클래스 레벨에 @ResponseBody을 작성해도된다.
그래서 이 @Controller와 @ResponseBody을 묶은 특화된 컨트롤러가 @RestController 어노테이션이다.
@RequestMapping("sub38")
@RestController
//@Controller
//@ResponseBody
public class Controller38 {
@Autowired
private Mapper02 mapper02;
@GetMapping("link20")
public List<String> method20() {
return mapper02.sql1();
}
@GetMapping("link21")
public List<Dto07> method21() {
return mapper02.sql3();
}
@GetMapping("link22")
public List<Dto08> method22(Integer cid) {
return mapper02.sql7(cid);
}
}그래서 페이지 로딩없이 요청보내고 받는 것은 알겠는데
사용자가 네트워크 열어서 보는 것은 아니다.
페이지에 뿌려줘야한다.
$("#btn22").click(function() {
const cid = $("#categoryIdInput").val();
$.ajax("/sub38/link22?cid=" + cid)
.done(function(data) {
const list = $("#productList"); //ul
list.empty();
data.forEach(function(product){
list.append(`<li>${product.productName}</li>`)
})
});
})혼자서 분석해보기
$.ajax(?)하면 데이터를 받아옴
받아온데이터를
.done(함수(data))를 사용하는 함수를 만듬
함수안에 data.forEach각각 돌려가는 함수를 만듬 -> ?
65. @ResponseBody success, error, complete -1
요청보내고 요청받을 수 있게 했는데 사용자에게 보여줘야한다.
jQuery.ajax("링크", setting) setting에 여러가지가 들어갈 수 있다.
contentType, method, data를 만졋었는데
이번엔 success, error, complete를 알아보자.
셋은 각각 함수 이다.
이것들을 각각 넣으면 요청을 보낸다음 응답이 완료됫을때 응답의 상태에 따라서
success, error, complete메소드가 실행된다.
잘됫으면 success와 complete고 틀리면 error메소드가 실행된다.
완료후 실행되는 함수를 settings에 넣으면된다.
success error complete를 프로퍼티로 넣고 값으로 함수를 넣는 것이다.
success은 성공시 error은 실패시 complete는 성공이든 실패든 실행된다.
$("#btn1").click(function(){
$.ajax("/sub39/link1", {
success: function (){
console.log("성공");
},
error : function (){
console.log("실패");
},
complete : function (){
console.log("성공이든 실패든");
}
});
})66. @ResponseBody success, error, complete -2
성공응답을 받는 버튼 성공(200) 응답을 받아서 콘솔에 성공!!!!출력하기
실패응답을 받는 버튼 실패(500) 응답을 받아서 콘솔에 실패!!!출력되게하기
ResponseEntity의 ok()메소드가 200 HTTP response status code를 발생시킨다.
ResponseEntity의 internalServerError() 메소드가 500 HTTP response status code를 발생시킨다.
$("#btn3").click(function(){
//성공(200) 응답을 받아서 콘솔에 성공!!!!출력하기
$.ajax("/sub39/link3", {
success : function () {
console.log("성공!!!");
}
});
})
$("#btn4").click(function(){
//실패(500) 응답을 받아서 콘솔에 실패!!!출력되게하기
$.ajax("/sub39/link4", {
error : function(){
console.log("실패!!!!");
}
});
})@GetMapping("link3")
@ResponseBody
public ResponseEntity method3() {
return ResponseEntity.ok().build();
}
@GetMapping("link4")
@ResponseBody
public ResponseEntity method4() {
return ResponseEntity.internalServerError().build();
}만약 service에게 일을 시켜서 값을 받는 다면 결과에 따라 200응답과 실패응답을 보낼 수 있을 것이다.
@GetMapping("link5")
public ResponseEntity method5() {
boolean ok = service.method();
if (ok) {
return ResponseEntity.ok();
} else {
return ResponseEntity.notFound().build();
}
}67. @ResponseBody done fail always
success, error, complete를 다른 방법으로도 사용할 수 있다.
done fail always 메소드다 .그런데 이녀석들으 jqXHR에 있는 메소드이다.
jqXHR은 ajax메소드가 리턴하는 값의 타입이다.
ajax메소드가 값을 리턴하면 done fail always메소드를 사용할 수 있다.
done메소드가 리턴하면 역시나 jqXHR을 리턴한다.
그래서 연속해서 사용할 수 있다.
각 메소드는 함수를 파라미터로 가질 수 있다.
done은 성공시 fail은 실패시 always은 성공이든 실패든 실행된다.
$("#btn5").click(function() {
$.ajax("/sub39/link1")
.done(function() {
console.log("done 메소드사용, 성공!!")
})
.fail(function() {
console.log("fail 메소드사용, 실패!!")
})
.always(function() {
console.log("always 메소드사용, 성공이든 실패든")
});
})68. @ResponseBody done fail always - 2
link3과 link4에 요청해서 각각 200번 500번 응답을 받아보자.
$("#btn7").click(function() {
$.ajax("/sub39/link3")
.done(function(){
console.log("성공!!!!");
});
})
$("#btn8").click(function() {
$.ajax("/sub39/link4")
.fail(function(){
console.log("실패!!!!");
});
})두가지방법이 있게 되는 것이다.
success, error, complete
vs
done fail always
어떤 것을 사용해도 같은 결과가 나온다.
회사마다 다르게 사용한다. 우리는 전자를 사용하도록 하자.
69. 응답 본문 처리
@GetMapping("link9")
@ResponseBody
public String method9() {
return "hello ajax!@!@";
}성공메소드의 파라미터를 넣으면된다.
ajax의 결과가 파라미터로 들어가게 된다.
$("#btn9").click(function() {
$.ajax("/sub39/link9", {
success: function(data) {
console.log("링크9 성공");
console.log(data); // hello ajax!@!@
}
});
})70. 응답 본문 처리 - 2
응답 본문(현재 시간)이 콘솔에 출력되게 하기
@GetMapping("link10")
@ResponseBody
public String method10() {
return LocalTime.now().toString();
}$("#btn10").click(function() {
//응답 본문(현재 시간)이 콘솔에 출력
$.ajax("/sub39/link10", {
success : function(data){
console.log(data);
}
});
})71. 응답 본문 처리 - 3
콘솔에 출력하는 것이 아닌 html에 출력해야한다.
태그의 값으로 넣어주면된다.
<div id="res10"></div>
$("#btn10").click(function() {
$.ajax("/sub39/link10", {
success : function(data){
$("#res10").text(data);
}
});
})72. 응답 본문 처리 - 4
복잡한 JSON을 처리해보자.
$("#btn11").click(function() {
$.ajax("/sub39/link11", {
success: function(data) {
$("#res11").text(data);
}
});
})
@GetMapping("link11")
@ResponseBody
public Map<String, Object> method11() {
return Map.of("name", "채소연", "age", 20);
}73. 응답 본문 처리 - 5
위는 기대한 값과 다르게 [object Object]이 출력된다.
응답한것은 JSON형식의 TEXT데이터가 맞는데 프로퍼티가 있는게 아닌 텍스트가 있는 것이다.
자바스크립트에서 쓰려면 다시 객체로 변환 시켜서 사용해야한다.
JSON.parse()메소드를 통해서 값을 파싱해서 사용해야한다.
JSON형태의 text를 객체 형태로 바꿔주는 메소드이다.
그런데 우리는 어차피 객체로 변환시킬 것이다. 무조건 JSON.parse()를 사용한다.
그래서 JSON의 ajax메소드가 파싱을 해줘서 [object Object]로 나오게 된 것이다.
$("#btn11").click(function() {
$.ajax("/sub39/link11", {
success: function(data) {
$("#res11").text(data.age + data.name);
}
});
})그럼 아까 String은 왜 파싱안해줬나?
만약 json이 아니라 그냥 문자열을 보냈다고 해보자.
파싱을 하지 못하고 {"":""}의 형태로 출력하게 된다.
contentType 헤더때문에 이런일이 발생하게 된다.
그냥 Stirng이면 contentType은 html/text
응답데이터가 json타입이면 json으로 인식하게 된다.
dataType으로 내가 무엇을 보내던 그냥 text인지 json인지 지정을 할 수 있다.
기본값이 Intelligent Guess이다. 알아서 추측한다는 것인데 응답의 MIME type을 보고 추측한다.
이것은 contentType의 값과 비슷한 것이다.
결국 JSON으로 객체를 포장해서 잘 보내주는게 중요한 것이다.
74. 응답 본문 처리 - 6
콘솔에 출력되도록 JSON을 보내는 코드를 작성해보자.
$("#btn12").click(function() {
$.ajax("/sub39/link12", {
success: function(data) {
console.log(data.address);
console.log(data.city);
console.log(data.phone);
}
});
})@GetMapping("link12")
@ResponseBody
public Map<String, Object> method12() {
return Map.of("address", "경기도", "city", "고양시", "phone", "갤럭시");
}75. 응답 본문 처리 - 7
List는 배열로 JSON이되어 가기때문에 FOR문으로 가져오면 된다.
@GetMapping("link13")
@ResponseBody
public List<String> method13() {
return List.of("강백호", "채치수", "송태섭"); // ["강백호", "채치수" , "송태섭"]
}$("#btn13").click(function() {
$.ajax("/sub39/link13", {
success: function(data) {
for (let i = 0; i < data.length; i++) {
console.log(data[i]);
}
}
});
})76. 응답 본문 처리 - 8
리턴되는 데이터가 객체일때
@Autowired
Mapper02 mapper02;
@GetMapping("link14")
@ResponseBody
public List<Dto08> method14(Integer cid) {
return mapper02.sql7(cid);
}$("#btn14").click(function() {
const cid = $("#categoryIdInput").val();
$.ajax("/sub39/link14?cid=" + cid, {
success: function(data) {
$("#res14").empty();
for (let i = 0; i < data.length; i++) {
$("#res14").append(data[i].productName + "</br>");
}
}
});
})77. 응답 본문 처리 - 9
위의 링크와 같은데 결과를 보여줄때 테이블 형식으로 보여지게하기
조합하는게 어렵다.
res15밑에 테이블, thead등 상단 넣기
res15자식 자식중 table클래스를 가진애에 tbody넣기 알아서 닫는코드 만들어줌.
res15밑의 tbody에 tr~td들 넣기의 순서대로 이루어진다.
까먹고잇던점 element들 간 부모 자식관계 나타내기 > 자식 그냥 공백 그 자식, 손자들중
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
백틱으로 쓴게 자바스크립트 Template literals이다.
값이 그대로 들어가게 된다.
$("#btn15").click(function() {
const cid = $("#categoryIdInput").val();
$.ajax("/sub39/link14?cid=" + cid, {
success: function(data) {
$("#res15").empty();
$("#res15").append(`
<table class="table">
<thead>
<tr>
<th>상품명</th>
<th>가격</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
`);
//$("#res15 > .table").append("<tbody>");
for (let i = 0; i < data.length; i++) {
$("#res15 tbody").append(`
<tr>
<td>${data[i].productName}</td>
<td>${data[i].price}</td>
</tr>
`);
}
}
});
})2023.05.12
comment entity가 있고 db에서 값을 가져와서
json으로 값을 보여주기 view에서 값을 가져와서 댓글로 만들기
프로퍼티 : 객체 넣어서 프로퍼티.객체.프로퍼티 이런느낌으로 꺼내는 건가?
service로 받아온 post요청을 매핑해서 보내서 db에저장하고
이것을 다시 받아와서 return하면 매핑된 dto가 가게되고 getXXX로 json데이터를 얻어내는 것 같다.
싱글톤 패턴이기 때문에 온거를 받아서 온거를 보내는 느낌이다.
@ResponseBody의 용도를 알게 되었다. 그래서 문자나 객체를 보내서 뭘할건데를 이해하게 되엇다.
강의들은 백엔드강의라서 쏘는 방법만 알려주는 것이었다.
이부부은 프론트엔드의 영역이다.
'국비 > Project - 1 게시판' 카테고리의 다른 글
| 2023.05.16 77일차-1 Project (0) | 2023.05.16 |
|---|---|
| 2023.05.15 76일차 Project (0) | 2023.05.16 |
| 2023.05.11 74일차 Project (0) | 2023.05.11 |
| 2023.05.10 73일차 Project (0) | 2023.05.11 |
| 2023.05.09 72일차 Project (0) | 2023.05.09 |