오늘 한 일
1. 수령완료 기능 구현
2. 글 상세페이지 완료
1. 수령완료 기능 구현
망망대해에서 시작하는 하루~~
일단 백엔드 남은 기능 수령완료 하고나서 프론트 다시 손봄
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
UserPost userPost = userPostRepository.findByPostAndUserAndRoleEquals(post, user, UserPostRole.PARTICIPANT).orElseThrow(()->
new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_PARTICIPANT)
);
List<Menu> menus = getUserMenus(user,post);
}
아 근데 헷갈렸는데
이 부분을 짜다가 리뷰를 발생시키고 다 끊으려고 했는데
리뷰가 생각과 다르게 그냥 Userid만 달려있는 리뷰인거임
이 경우에는 Order 테이블이 있어야 할 거 같음
리뷰 작성을 위해…
Order테이블을 Review작성시 조회해오면 될듯, 그거보고 리뷰 남기면 될 거 같음
Order Entity 짜다가
어 이거 보낸사람이 Host인거랑 Participant인거랑 테이블 구분되어야 하는거 아냐..?
해서 Entity 두개로 만들려다가
그냥 senderRoleEnum 만들어서 보낸사람이 Participant인지 Host인지 구분하는 칼럼 하나 추가하기로 함,
처음에는 받은사람 Role 추가하려 했는데 리뷰 작성시킬때 어차피 본인의 Role 기준으로 띄우니까 저게 더 알아보기 편할 거 같았음
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "tb_order")
public class Order extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String store;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<Menu> menus;
@JoinColumn(name = "sender_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@JoinColumn(name = "receiver_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private User receiver;
@Builder
public Order(User user, User receiver, String store) {
this.user = user;
this.receiver = receiver;
this.store = store;
}
}
이렇게 짜봄!
그리고 Menu에 Order를 넣어좀!
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "tb_menu")
public class Menu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column
private String menuname;
@Column
private Integer price;
@Builder
public Menu(Post post, String menuname, Integer price, User user) {
this.post = post;
this.menuname = menuname;
this.price = price;
this.user = user;
}
}
그리고 수령완료 처리할때 post의 menus를 타고들어가서 menu들에 orderId부여하기로 함!
menu랑 post연관관계 삭제할때
참가자들의 Menu들만 있는게 아니면 menus 타고들어가서도 UserPost의 Role이 있는지 없는지 또 판별하고 있어야 해서
그 전 로직을 수정해봄
모집마감을 하고나면 어차피 더이상 메뉴 작성이 안 되니까 모집마감을 하면서 Participant가 아닌 User들의 menu를 모두 삭제하게 바꿈
@Override
public void closeApplication(PostIdRequest postIdReq, User user) {
//check if there is a post with the post id
Post post = getPostById(postIdReq.postId());
//check if the user is the host of the post
checkIfHost(user,post);
//check if the status is OPEN
if(post.getPostStatus()!=PostStatusEnum.OPEN){
throw new GlobalException(PostErrorCode.POST_ALREADY_CLOSED);
}
//delete all menus which are not made by particimants
for()
post.closeApplication();
postRepository.save(post);
}
하다보니 userpost들의 List가 필요한데
위에 checkIfHost가 UserPostRepository를 한번 돌아오길래 그부분도 UserPost db를 한번만 조회할 수 있도록 바꿈
@Override
public void closeApplication(PostIdRequest postIdReq, User user) {
//check if there is a post with the post id
Post post = getPostById(postIdReq.postId());
//check if the user is the host of the post
List<UserPost> userPosts = getUserPostsByPost(post);
User host = getAuthor(userPosts);
checkIfHost(user,post);
//check if the status is OPEN
if(post.getPostStatus()!=PostStatusEnum.OPEN){
throw new GlobalException(PostErrorCode.POST_ALREADY_CLOSED);
}
//delete all menus which are not made by particimants
for()
getUserMenus(user,post)
post.closeApplication();
postRepository.save(post);
}
그리고 기준의 checkIfHost를 오버로딩해서 하나 더 만듦
private void checkIfHost(User user, Post post){
if(!userPostRepository.existsByUserIdAndPostIdAndRole(user.getId(), post.getId(), UserPostRole.HOST)){
throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_HOST);
}
}
private void checkIfHost(User user, User host){
if(!user.getId().equals(host.getId())){
throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_HOST);
}
}
그리고 checkIfHost를 post를 들고가서 UserPostRepository한번 더 들고가지 않고 그냥 User 들고가서 둘이 비교하게 오버로딩한걸 씀
//check if the user is the host of the post
List<UserPost> userPosts = getUserPostsByPost(post);
User host = getAuthor(userPosts);
checkIfHost(user,host);
일단 그 Post의 모든 메뉴를 가져와서 걸러내고 삭제함
//delete all menus which are not made by participants
List<Menu> menus = menuRepository.findAllByPost(post);
List<Menu> menusWithoutRelation;
for(Menu menu : menus){
boolean hasRelation = false;
for(UserPost userPost:userPosts){
if(userPost.getUser().getId().equals(menu.getUser().getId())){
hasRelation = true;
break;
}
}
if(!hasRelation){
menuRepository.delete(menu);
}
}
테스트해봄
@Override
public void closeApplication(PostIdRequest postIdReq, User user) {
//check if there is a post with the post id
Post post = getPostById(postIdReq.postId());
//check if the user is the host of the post
List<UserPost> userPosts = getUserPostsByPost(post);
User host = getAuthor(userPosts);
checkIfHost(user,host);
//check if the status is OPEN
if(post.getPostStatus()!=PostStatusEnum.OPEN){
throw new GlobalException(PostErrorCode.POST_ALREADY_CLOSED);
}
//delete all menus which are not made by participants
List<Menu> menus = menuRepository.findAllByPost(post);
List<Menu> menusWithoutRelation;
for(Menu menu : menus){
boolean hasRelation = false;
for(UserPost userPost:userPosts){
if(userPost.getUser().getId().equals(menu.getUser().getId())){
hasRelation = true;
break;
}
}
if(!hasRelation){
menuRepository.delete(menu);
}
}
post.closeApplication();
postRepository.save(post);
}
1은 참가자고 2는 host고 3은 아무 관계없는 사람임
모집 마감을 해보겠음
{
"status": 200,
"message": "모집이 마감되었습니다.",
"data": null
}
음 ~ 3것만 다 사라짐 !
테스트 완료!
다시 수령완료 기능 개발함
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
UserPost userPost = userPostRepository.findByPostAndUserAndRoleEquals(post, user, UserPostRole.PARTICIPANT).orElseThrow(()->
new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_PARTICIPANT)
);
List<Menu> menus = getUserMenus(user,post);
}
UserPost 테이블 이미 Host 판별할때 조회해 왔으니까
UserPost 테이블 조회를 줄이기 위해서 그거가지고 Participant인지 구분도 해봄
private void checkIfParticipant(User user, List<UserPost> userPosts){
for(UserPost userPost : userPosts){
if(userPost.getRole().equals(UserPostRole.HOST)){
continue;
}
if(user.getId().equals(userPost.getUser().getId())){
return;
}
}
throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_PARTICIPANT);
}
package com.moayo.moayoeats.backend.domain.order.entity;
public enum SenderRole {
HOST,
PARTICIPANT
}
이거 만들어서
@Column
@Enumerated(EnumType.STRING)
private SenderRole senderRole;
Order에다 넣어놓고 생각해보니 UserPostRole이랑 똑같은데 그냥 그거 갖다쓰면 될 거 같음
@Column
@Enumerated(EnumType.STRING)
private UserPostRole senderRole;
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
List<UserPost> userPosts = getUserPostsByPost(post);
checkIfParticipant(user, userPosts);
User host = getAuthor(userPosts);
Order order = Order.builder()
.receiver(host)
.user(user)
.store(post.getStore())
.senderRole(UserPostRole.PARTICIPANT)
.build();
orderRepository.save(order);
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menu.receive(order));
}
→
저거 하다가 Menu에
orderId 저장하고
post는 null로 만드는 과정 수행하는 메서드 만듦
class Menu
public void receive(Order order){
this.order = order;
this.post = null;
}
forEach문 짜다가 애매해져서 그냥 receive가 Menu를 리턴하게 만듦
public Menu receive(Order order){
this.order = order;
this.post = null;
return this;
}
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menuRepository.save(menu.receive(order)));
if(post.getMenus().size())
그리고 이부분 파트 짜는데 또 생각이 드는게
위에서 getUserMenus에서 돌았는데 저기서 MenuRepository를 또 PostId로 돌게되지 않나.. 하는 생각!
저걸로 모든 참가자들이 수령완료를 했는지 판별하려고 했는데 생각해보니 작성자의 메뉴는 아직 남아있는 상태라서 저렇게 하면 안됨!
생각해보니 userPost를 조회해온걸로 모든 사람이 수령완료를 했는지 알 수 있을듯해서 해봄
이렇게 하려는데
userPostRepository.delete();
아까 이렇게 짠 거에서 UserPost를 돌려보내면 그거 쓰면 될 거 같아서 걍 그거 써봄
private void checkIfParticipant(User user, List<UserPost> userPosts){
for(UserPost userPost : userPosts){
if(userPost.getRole().equals(UserPostRole.HOST)){
continue;
}
if(user.getId().equals(userPost.getUser().getId())){
return;
}
}
throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_PARTICIPANT);
}
->
private UserPost getUserPostByUserIfParticipant(User user, List<UserPost> userPosts){
for(UserPost userPost : userPosts){
if(userPost.getRole().equals(UserPostRole.HOST)){
continue;
}
if(user.getId().equals(userPost.getUser().getId())){
return userPost;
}
}
throw new GlobalException(PostErrorCode.FORBIDDEN_ACCESS_PARTICIPANT);
}
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
List<UserPost> userPosts = getUserPostsByPost(post);
checkIfParticipant(user, userPosts);
User host = getAuthor(userPosts);
Order order = makeOrder(post, host, user, UserPostRole.PARTICIPANT);
orderRepository.save(order);
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menuRepository.save(menu.receive(order)));
userPostRepository.delete();
}
→
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
List<UserPost> userPosts = getUserPostsByPost(post);
UserPost userpost = getUserPostByUserIfParticipant(user, userPosts);
User host = getAuthor(userPosts);
Order order = makeOrder(post, host, user, UserPostRole.PARTICIPANT);
orderRepository.save(order);
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menuRepository.save(menu.receive(order)));
userPostRepository.delete(userpost);
}
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
//get relations
List<UserPost> userPosts = getUserPostsByPost(post);
UserPost userpost = getUserPostByUserIfParticipant(user, userPosts);
User host = getAuthor(userPosts);
//make order for me to review
Order order = makeOrder(post, host, user, UserPostRole.PARTICIPANT);
orderRepository.save(order);
//make order for host to review
Order hostOrder = makeOrder(post, user, host, UserPostRole.HOST);
orderRepository.save(hostOrder);
//remove menus from the post
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menuRepository.save(menu.receive(order)));
if(userPosts.size()<=2){
userPostRepository.deleteAll(userPosts);
postRepository.delete(post);
return;
}
userPostRepository.delete(userpost);
}
수령완료할때 사이즈가 2 이하가 되면 (나랑 호스트만 남음) 삭제함
일단 완성됐다고 생각해서 테스트해봄!
//수령 완료
@DeleteMapping("/posts/received")
public ApiResponse<Void> receiveOrder(
@AuthenticationPrincipal UserDetailsImpl userDetails,
@Valid @RequestBody PostIdRequest postIdReq
) {
postService.receiveOrder(postIdReq, userDetails.getUser());
return new ApiResponse<>(HttpStatus.OK.value(), "수령완료 처리가 되었습니다.");
}
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
//get relations
List<UserPost> userPosts = getUserPostsByPost(post);
UserPost userpost = getUserPostByUserIfParticipant(user, userPosts);
User host = getAuthor(userPosts);
//make order for me to review
Order order = makeOrder(post, host, user, UserPostRole.PARTICIPANT);
orderRepository.save(order);
//make order for host to review
Order hostOrder = makeOrder(post, user, host, UserPostRole.HOST);
orderRepository.save(hostOrder);
//remove menus from the post
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menuRepository.save(menu.receive(order)));
if(userPosts.size()<=2){
userPostRepository.deleteAll(userPosts);
postRepository.delete(post);
return;
}
userPostRepository.delete(userpost);
}
🚩문제 발생: Menu와 Post의 관계를 끊고 Menu를 옮기려는데 옮겨지지 않음
에러남
근데 왜? order는 비어있어도 괜찮잖아
java.sql.SQLIntegrityConstraintViolationException: Column 'post_id' cannot be null at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118) ~[mysql-connector-j-8.1.0.jar:8.1.0]
단방향에서만 제거해줘서 그런가?
Menu를 가보니 이렇게 되어있음
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "post_id", nullable = false)
private Post post;
nullable = false를 제거해봄!
java.sql.SQLIntegrityConstraintViolationException: Column 'post_id' cannot be null at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:118) ~[mysql-connector-j-8.1.0.jar:8.1.0]
음 단방향에서만 제거해서 그럴 수 있음
양방향에서 제거해봄
Post에 이거 만듦
public void dismissMenu(Menu menu){
this.menus.remove(menu);
}
Menu의 receive 메서드 수정함
public Menu receive(Order order){
this.order = order;
this.post.dismissMenu(this);
this.post = null;
return this;
}
다시 테스트해봄
{
"status": 200,
"message": "수령완료 처리가 되었습니다.",
"data": null
}
오오 !!!!
됐고 Post도 사라졌는데 문제가
menu가 싹 사라짐 ㅜㅜ 저렇게 끊는다고 cascade가 안 되는게 아닌가봄…?
아님 안 끊겼거나 ㅜㅜ
@Transactional 붙여보긴 했는데 그거 아닌거 같고 post를 따로 save를 해봐야 할 거 같음!
class Post{
@Transactional
public void dismissMenu(Menu menu){
this.menus.remove(menu);
}
이거랑
Service의 이걸로 하고 있었는데 이거 아닌거 같다!
@Transactional
@Override
public void receiveOrder(PostIdRequest postIdReq, User user) {
Post post = getPostById(postIdReq.postId());
//get relations
List<UserPost> userPosts = getUserPostsByPost(post);
UserPost userpost = getUserPostByUserIfParticipant(user, userPosts);
User host = getAuthor(userPosts);
//make order for me to review
Order order = makeOrder(post, host, user, UserPostRole.PARTICIPANT);
orderRepository.save(order);
//make order for host to review
Order hostOrder = makeOrder(post, user, host, UserPostRole.HOST);
orderRepository.save(hostOrder);
//remove menus from the post
List<Menu> menus = getUserMenus(user,post);
menus.forEach(menu->menuRepository.save(menu.receive(order)));
if(userPosts.size()<=2){
userPostRepository.deleteAll(userPosts);
postRepository.delete(post);
return;
}
userPostRepository.delete(userpost);
}
issue : 🚀근데 저거 하다가 여태까지의 id 시스템에 회의를 가지게 됨
이미 없어진 id는 또 써도 되지 않나?
왜 2번을 삭제하고 또 만들면 3번이 만들어지지?
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
(strategy = GenerationType.IDENTITY)
왠지 이거일거 같다
📖Identifiers in Hibernate/JPA
일단 수령완료기능 오늘은 여기까지 하고 나중에 추가로 메뉴 삭제하지 않고 남기고 Order에 주기, 스케쥴링삭제기능 구현후 모두 수령했을때 post를 삭제하지 않고 상태를 received로 수정하기 할거임!
2. 글 상세페이지 구현
🚀issue : 좌표를 받아와서 마커찍는것이 안 됨
망망대해 프론트로 다시 돌아옴
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>글 상세페이지</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=sa2ld1fdvv"></script>
</head>
<body>
<div id="map" style="width:100%; height:800px;"></div>
<script th:inline="javascript">
var res;
var latitude;
var longitude;
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(latitude, longitude),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(latitude, longitude),
map: map
});
$( document ).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/5`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
latitude =$('latitude');
longitude = $('longitude');
},
error: function (error) {
console.error('Error:', error);
}
});
}
</script>
</body>
</html>
받아오긴 하는데 이걸 읽지를 못하는 상황!
If I understand correctly with the object up top being the JSON response, I think you want this...
{"id":"","livre":"empty_name"}
$.ajax({
url: "sent.php",
type: "post",
dataType: "json",
data: formdata,
success: function (data) {
var jsonId = data.id;
}
});
The data parameter of the success callback contains your response (in this case, JSON data). You access your JSON content there.
StackOverFlow 참고해서 해봄
<script th:inline="javascript">
var data;
var latitude;
var longitude;
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(latitude, longitude),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(latitude, longitude),
map: map
});
$( document ).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/5`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude =response.data.latitude;
longitude = response.data.latitude;
},
error: function (error) {
console.error('Error:', error);
}
});
}
</script>
이렇게 해봄
여전히 망망대해~
longitude = response.data.latitude;
오타 수정함
longitude = response.data.longitude;
로그 찍어봄
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude =response.data.latitude;
longitude = response.data.longitude;
console.log(latitude);
console.log(longitude);
},
일단 데이터 받아온건 문제가 아니라는걸 알겠음
저게 왜 안 찍히냐가 문제임 이제는
naver.maps.LatLng
https://navermaps.github.io/maps.js.ncp/docs/naver.maps.LatLng.html
number를 받는다고 함 ㅜㅜ….
javascript의 number는 따로 double이렇게 안 나누고 다 소숫점이 있고 무한대라고 함
근데 왜 안 들어가나요?
💡예측해봄
저게 제일 먼저 출력되어서 latitude,longitude가 들어가기 전에 출력되었을 가능성을
<script th:inline="javascript">
var data;
var latitude;
var longitude;
$( document ).ready(function() {
getData();
drawMap();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/5`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude =response.data.latitude;
longitude = response.data.longitude;
console.log(latitude);
console.log(longitude);
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap() {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(latitude, longitude),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(latitude, longitude),
map: map
});
}
</script>
순서를 바꿔봄
여전히 망망대해임
function drawMap() {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(37.5705221, 126.9639472),
zoom: 15
});
이렇게 쓰면 들어가고
이렇게 변수로 하면 안 들어감
<script th:inline="javascript">
var data;
var latitude;
var longitude;
$( document ).ready(function() {
getData();
drawMap();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude =response.data.latitude;
longitude = response.data.longitude;
console.log(latitude);
console.log(longitude);
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap() {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(latitude, longitude),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(latitude, longitude),
map: map
});
}
</script>
변수가 왜 안들어가냐고요
💡전역변수 말고 전달로 한번 해봄
<script th:inline="javascript">
var data;
var latitude;
var longitude;
$( document ).ready(function() {
getData();
//drawMap();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude =response.data.latitude;
longitude = response.data.longitude;
console.log(latitude);
console.log(longitude);
drawMap(latitude,longitude)
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap(lat,lng) {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(lat, lng),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lng),
map: map
});
}
</script>
헐 전역변수로 안 하고 직접 전달하니까 나와짐~!!!
⛳해결: 전역변수로 안 하고 method의 parameter로 전달하니까 값이 전달됨
<script th:inline="javascript">
var data;
$( document ).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude =response.data.latitude;
longitude = response.data.longitude;
console.log(latitude);
console.log(longitude);
drawMap(latitude,longitude)
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap(lat,lng) {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(lat, lng),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lng),
map: map
});
}
</script>
와 됐다~~
이제 받아온 다른 값들도 화면에 출력해주면 됨!!
- address: "37.5705221,126.9639472"
- deadline: "2024-01-15T15:28:18"
- deliveryCost: 123
- latitude: 37.5705221
- longitude: 126.9639472
- menus: [{…}]
- minPrice: 123
- store: "123"
- sumPrice: 0
화면에 일단 menus 빼고는 다 달아봤는데 안 나온다!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>글 상세페이지</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=sa2ld1fdvv"></script>
</head>
<body>
<div style="padding:10px;width:1000px;height:300px;">
<div>주소</div>
<div id="map" style="width:100%; height:800px;"></div>
</div>
<div style="padding:10px;width:1000px;">
<div>가게</div>
<div th:text="${store}">가게</div>
<div>합계 금액</div>
<div th:text="${sumPrice}">합계금액</div>
<div>최소주문금액</div>
<div th:text="${minPrice}">최소주문금액</div>
<div>배달비</div>
<div th:text="${deliveryCost}">배달비</div>
<div>마감 기한</div>
<div th:text="${deadline}">마감 기한</div>
</div>
<script th:inline="javascript">
var data;
var latitude;
var longitude;
var deadline;
var deliveryCost;
var minPrice;
var store;
var sumPrice;
$( document ).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude = response.data.latitude;
longitude = response.data.longitude;
deadline = response.data.deadline;
deliveryCost = response.data.deliveryCost;
minPrice = response.data.minPrice;
store = response.data.store;
sumPrice = response.data.sumPrice;
drawMap(latitude,longitude)
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap(lat,lng) {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(lat, lng),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lng),
map: map
});
}
</script>
</body>
</html>
주소만 나옴…
그리고 지도 크기를 좀 줄여야 할 거 같기도
지도 위에만 출력되어서 주소만 써있나 싶어서 다시 써봄
그리고 height가 없었어서 max-content로 한번 해봄
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>글 상세페이지</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script type="text/javascript" src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=sa2ld1fdvv"></script>
</head>
<body>
<div style="padding:10px;width:1000px; height:max-content">
<div>가게</div>
<div th:text="${store}">가게</div>
<div>합계 금액</div>
<div th:text="${sumPrice}">합계금액</div>
<div>최소주문금액</div>
<div th:text="${minPrice}">최소주문금액</div>
<div>배달비</div>
<div th:text="${deliveryCost}">배달비</div>
<div>마감 기한</div>
<div th:text="${deadline}">마감 기한</div>
</div>
<div style="padding:10px;width:1000px;height:300px;">
<div>주소</div>
<div id="map" style="width:100%; height:200px;"></div>
</div>
<script th:inline="javascript">
var data;
var latitude;
var longitude;
var deadline;
var deliveryCost;
var minPrice;
var store;
var sumPrice;
$( document ).ready(function() {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:',response);
data = response.data;
latitude = response.data.latitude;
longitude = response.data.longitude;
deadline = response.data.deadline;
deliveryCost = response.data.deliveryCost;
minPrice = response.data.minPrice;
store = response.data.store;
sumPrice = response.data.sumPrice;
drawMap(latitude,longitude)
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap(lat,lng) {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(lat, lng),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lng),
map: map
});
}
</script>
</body>
</html>
ㅜㅜ 안 뜸!!!
<div style="padding:10px;width:1000px; height:max-content">
<div>가게</div>
<div th:text="${store}">가게</div>
<div>합계 금액</div>
<div th:text="${sumPrice}">합계금액</div>
<div>최소주문금액</div>
<div th:text="${minPrice}">최소주문금액</div>
<div>배달비</div>
<div th:text="${deliveryCost}">배달비</div>
<div>마감 기한</div>
<div th:text="${deadline}">마감 기한</div>
</div>
그래도 이제 이걸 어떻게 하면 뜨게 할 수 있는지만 하면 될 거 같다!
다시보니 이거 선언을 안 해줬음
<html>
→
그래도 똑같음
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#standard-expression-syntax
여기 들어가서 용법을 다시 살펴봄!
Simple expressions:
Variable Expressions: ${...}
Selection Variable Expressions: *{...}
Message Expressions: #{...}
Link URL Expressions: @{...}
Fragment Expressions: ~{...}
문법이 맞아 보이는데 왜 안되나고요 ㅜㅜㅜ
<div style="padding:10px;width:1000px; height:max-content">
<div>가게</div>
<div th:text="${store}">가게</div>
<div>합계 금액</div>
<div th:text="${sumPrice}"></div>
<div>최소주문금액</div>
<div th:text="${minPrice}"></div>
<div>배달비</div>
<div th:text="${deliveryCost}"></div>
<div>마감 기한</div>
<div th:text="${deadline}"></div>
</div>
이렇게 한번 해봄
여전히 안 됨
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#variables
이것도 읽어봄
그냥 div로 만들어서 붙여버려야겠다
문제가 뭔지 알았음
script 안에 내가 만들어둔거 클릭하면 그 안에서만 연결되어서 하이라이트 쳐지고 위에 store 변수는 안 쳐지는거 보니까 scope를 벗어나서 인식을 못하나봄!
→ ❓ Q. 왜?
<div id="map" style="width:100%; height:500px;"></div>
map도 생성부분을 script의 함수 안으로 옮기니까 뜨긴 했었음
<div id="field" style="width:1000px; height:min-content;"></div>
이렇게 달아보고
예전에 방명록 개발할때처럼 요소를 만들어서 그냥 달아보기로 함
let temp = `
<div class="log">
<div class="input-group mb-3">
<span class="input-group-text">${object.date}</span>
<span class="input-group-text">${object.name}</span>
<span><input type="text" id="deleteLogPw" placeholder="Password" aria-label="Password" class="form-control"></span>
<button class="btn btn-secondary" type="button" id="deleteLogBtn">삭제</button>
<div class="logText" id = ${child.ref.key}>
<span><img class="pfp" , src="./image/minsun/pfp/${object.pfp}.png" , alt="pfp"></span>
${object.content}
</div>
</div>
</div>
`
$('#logBox').append(temp)
예전에 내가 작성한 코드
→
응용해서 코드 작성
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
name="viewport">
<title>글 상세페이지</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=sa2ld1fdvv"
type="text/javascript"></script>
</head>
<body>
<div style="padding:10px;width:1000px; height:max-content">
<div id="field" style="width:1000px; height:min-content;"></div>
<div>합계 금액</div>
<div th:text="${sumPrice}"></div>
<div>최소주문금액</div>
<div th:text="${minPrice}"></div>
<div>배달비</div>
<div th:text="${deliveryCost}"></div>
<div>마감 기한</div>
<div th:text="${deadline}"></div>
</div>
<div style="padding:10px;width:1000px;height:min-content;">
<div>주소</div>
<div id="map" style="width:100%; height:500px;"></div>
</div>
<script th:inline="javascript">
var data;
var latitude;
var longitude;
var deadline;
var deliveryCost;
var minPrice;
var store;
var sumPrice;
$(document).ready(function () {
getData();
drawField();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:', response);
data = response.data;
latitude = response.data.latitude;
longitude = response.data.longitude;
deadline = response.data.deadline;
deliveryCost = response.data.deliveryCost;
minPrice = response.data.minPrice;
store = response.data.store;
sumPrice = response.data.sumPrice;
drawMap(latitude, longitude);
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap(lat, lng) {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(lat, lng),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lng),
map: map
});
}
function drawField() {
var temp = `
<div>
<div>가게</div>
<div th:text="${store}"></div>
<div>합계 금액</div>
<div th:text="${sumPrice}"></div>
<div>최소주문금액</div>
<div th:text="${minPrice}"></div>
<div>배달비</div>
<div th:text="${deliveryCost}"></div>
<div>마감 기한</div>
<div th:text="${deadline}"></div>
</div>
`
$('#field').append(temp)
}
</script>
</body>
</html>
또 안 나옴
얘도 지도때처럼 그냥 parameter로 전달해봄
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
name="viewport">
<title>글 상세페이지</title>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=sa2ld1fdvv"
type="text/javascript"></script>
</head>
<body>
<div style="padding:10px;width:1000px; height:max-content">
<div id="field" style="width:1000px; height:min-content;"></div>
</div>
<div style="padding:10px;width:1000px;height:min-content;">
<div>주소</div>
<div id="map" style="width:100%; height:500px;"></div>
</div>
<script th:inline="javascript">
var data;
var latitude;
var longitude;
var deadline;
var deliveryCost;
var minPrice;
var store;
var sumPrice;
$(document).ready(function () {
getData();
});
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/3`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:', response);
data = response.data;
latitude = response.data.latitude;
longitude = response.data.longitude;
deadline = response.data.deadline;
deliveryCost = response.data.deliveryCost;
minPrice = response.data.minPrice;
store = response.data.store;
sumPrice = response.data.sumPrice;
drawMap(latitude, longitude);
drawField(store,sumPrice,minPrice,deliveryCost,minPrice,deadline);
},
error: function (error) {
console.error('Error:', error);
}
});
}
function drawMap(lat, lng) {
var map = new naver.maps.Map('map', {
center: new naver.maps.LatLng(lat, lng),
zoom: 15
});
var marker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lng),
map: map
});
}
function drawField(store,sumPrice,minPrice,deliveryCost,minPrice,deadline) {
var temp = `
<div>
<div>가게</div>
<div th:text="${store}"></div>
<div>합계 금액</div>
<div th:text="${sumPrice}"></div>
<div>최소주문금액</div>
<div th:text="${minPrice}"></div>
<div>배달비</div>
<div th:text="${deliveryCost}"></div>
<div>마감 기한</div>
<div th:text="${deadline}"></div>
</div>
`
$('#field').append(temp)
}
</script>
</body>
</html>
전혀 뜨지 않죠~
<div>
<div>가게</div>
<div>${store}</div>
<div>합계 금액</div>
<div>${sumPrice}</div>
이렇게 바꿔봄
와 뜬다!!!!
마저 다 작성하고 해봄!
function drawField(store,sumPrice,minPrice,deliveryCost,minPrice,deadline) {
var temp = `
<div>
<div>가게</div>
<div>${store}</div>
<div>합계 금액</div>
<div>${sumPrice}</div>
<div>최소주문금액</div>
<div>${minPrice}</div>
<div>배달비</div>
<div>${deliveryCost}</div>
<div>마감 기한</div>
<div>${deadline}</div>
</div>
`
$('#field').append(temp)
}
와 나온다~~
이제 메뉴들만 뽑으면 됨
🚀issue: 이중 List구조 DTO 받아서 html에 element 생성해서 붙이기
public record DetailedPostResponse(
String address,
Double latitude,
Double longitude,
String store,
Integer minPrice,
Integer deliveryCost,
List<NickMenusResponse> menus,
Integer sumPrice,
LocalDateTime deadline
public record NickMenusResponse(
String nickname,
List<MenuResponse> menus
) {
}
public record MenuResponse(
String menuname,
Integer price
) {
}
이중구조의 forEach로 돌려보내는거 생각보다 어려워서 걍 모인메뉴 dto수정하고 싶어짐 ㅋㅋㅋㅋㅋ
function drawMenus(nickmenus) {
var count = 0;
nickmenus.forEach((nickmenu)=>
$('#menus').append(
`
<div>닉네임</div>
<div>${nickmenu.nickname}</div>
${nickmenu.menus.forEach((menu)=>drawMenu(menu))}
`
)
)
}
function drawMenu(menu){
var temp =
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
return temp;
)
}
저런식으로 대충 짜봤는데 return temp가 될리가 없고
count로 id동적으로 생성해서 달아주고 거기다 메뉴를 달아보기로 함 ㅋ ㅋ ㅋ
${nickmenu.menus.forEach((menu)=>drawMenu(menu))}
function drawMenu(menu){
var temp =
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
return temp;
)
}
일단 닉네임 출력부터 테스트해봄
일단 닉네임은 잘 나옴
참가자 추가한다음에 또 테스트해봄
잘 나온다!
이제 메뉴를 달아봄
function drawMenus(nickmenus) {
var count = 1;
nickmenus.forEach((nickmenu)=>
$('#menus').append(
`
<div>닉네임</div>
<div id = ${count++}>${nickmenu.nickname}</div>
`
)
)
count = 1;
nickmenus.forEach((nickmenu)=>
nickmenu.menus.forEach(
(menu)=>
$(`#${count}`).append(
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
)
),count++
)
}
지금 사람이랑 메뉴랑 따로놀고있긴 한데…
통새우버거 가 유저1꺼고
통새우버거3 이 유저3꺼임
아무튼 되긴 되는데 count가 안먹히는 느낌?
function drawMenus(nickmenus) {
var count = 1;
nickmenus.forEach((nickmenu)=>
$('#menus').append(
`
<div>닉네임</div>
<div id = ${count++}>${nickmenu.nickname}</div>
`
)
)
count2 = 1;
nickmenus.forEach((nickmenu)=>
nickmenu.menus.forEach(
(menu)=>
$(`#${count2}`).append(
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
)
),count2++
)
}
이렇게 해봄
ㅋㅋ 2에만 달림
유저 123에 고루고루 들어가야되는데 2에만 막 달림~~
왜 2에 달리냐고요…
function drawMenus(nickmenus) {
var count = 1;
nickmenus.forEach((nickmenu)=>
$('#menus').append(
`
<div>닉네임</div>
<div id = ${count++}>${nickmenu.nickname}</div>
`
)
)
index = 1;
nickmenus.forEach((nickmenu)=>
nickmenu.menus.forEach(
(menu)=>
$(`#${index}`).append(
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
)
),index++
)
}
→
function drawMenus(nickmenus) {
var count = 1;
nickmenus.forEach((nickmenu)=>
$('#menus').append(
`
<div>닉네임</div>
<div id = ${count}>${nickmenu.nickname}</div>
`
),
$(`#${count}`).append(
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
),
count++
)
}
응 이렇게는 안됨~
index = 1;
nickmenus.forEach((nickmenu,index++)=>
nickmenu.menus.forEach(
(menu)=>
$(`#${index}`).append(
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
)
)
)
이렇게 해봤는데 빨간줄이 뜸!
⛳해결 : forEach의 index를 이용해서 동적으로 element에 id 지정해줌
💡foreach의 두번째 인자에 List의 index를 넣을 수 있다고 함!
function drawMenus(nickmenus) {
nickmenus.forEach((nickmenu,index)=>
$('#menus').append(
`
<div>닉네임</div>
<div id = ${index}>${nickmenu.nickname}</div>
`
)
)
nickmenus.forEach((nickmenu,index)=>
nickmenu.menus.forEach(
(menu)=>
$(`#${index}`).append(
`<div>${menu.menuname}</div>
<div>${menu.price}</div>`
)
)
)
잘 됨
위의것도 index로 수정해봄
잘 됨
🚀issue: thymeleaf FrontController에서 html로 값 전달해주기
이제 페이지간 parameter전달만 해주면 되겠다
Model을 돌려보내보기로 함
@GetMapping("/test/readpost/{postId}")
public String readpostPage(
@PathVariable(name = "postId") Long postId
){
return "domain/post/readpost";
}
@GetMapping("/test/readpost/{postId}")
public String readpostPage(
@PathVariable(name = "postId") Long postId
){
Model model;
model.addAllAttributes("postId",postId);
return "domain/post/readpost";
}
빨간줄이 감
https://www.thymeleaf.org/doc/articles/springmvcaccessdata.html
이거 읽어봄
@GetMapping("/test/readpost/{postId}")
public String readpostPage(
@PathVariable(name = "postId") Long postId,
Model model
){
model.addAllAttributes("postId",postId);
return "domain/post/readpost";
}
이렇게 해봄
인텔리제이가 Cannot resolve method 'addAllAttributes(String, Long) 라고 함
딱히 검색해봐도 뭐 안 나옴
https://www.baeldung.com/spring-mvc-and-the-modelattribute-annotation
밸덩 읽어봄
3.2. The Controller
Here’s the controller class, where we’ll implement the logic for the aforementioned view:
@Controller
@ControllerAdvice
publicclassEmployeeController {
private Map<Long, Employee> employeeMap =newHashMap<>();
@RequestMapping(value = "/addEmployee", method = RequestMethod.POST)
public Stringsubmit(
@ModelAttribute("employee") Employee employee,
BindingResult result, ModelMap model) {
if (result.hasErrors()) {
return "error";
}
model.addAttribute("name", employee.getName());
model.addAttribute("id", employee.getId());
employeeMap.put(employee.getId(), employee);
return "employeeView";
}
@ModelAttribute
publicvoidaddAttributes(Model model) {
model.addAttribute("msg", "Welcome to the Netherlands!");
}
}Copy
In the submit() method, we have an Employee object bound to our View. We can map our form fields to an object model as simply as that. In the method, we’re fetching values from the form and setting them to ModelMap.
In the end, we return employeeView, which means that we call the respective JSP file as a View representative.
Furthermore, there’s also an addAttributes() method. Its purpose is to add values in the Model that’ll be identified globally. That is, every request to every controller method will return a default value as a response. We also have to annotate the specific class as @ControllerAdvice.
How to pass a variable value to HTML using Java and Thymeleaf
RestController와 Controller의 차이
https://www.thymeleaf.org/doc/articles/springmvcaccessdata.html
이거 읽고 해봄
@GetMapping("/test/createpost")
public String createPostPage(){
return "domain/post/createpost";
}
//@GetMapping("/test/readpost/{postId}")
@RequestMapping(value = "postId", method = RequestMethod.GET)
public String readpostPage(
@PathVariable(name = "postId") Long postId
){
ModelAndView mav = new ModelAndView("postId");
mav.addObject("postId",postId);
return "domain/post/readpost";
}
일단 이렇게 해봄
function getData() {
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/${postId}`,
dataType: "json",
contentType: 'application/json',
data: {},
success: function (response) {
console.log('Success:', response);
latitude = response.data.latitude;
longitude = response.data.longitude
deadline = response.data.deadline;
deliveryCost = response.data.deliveryCost;
minPrice = response.data.minPrice;
store = response.data.store;
sumPrice = response.data.sumPrice;
nickmenus = response.data.menus;
drawMap(latitude, longitude);
drawField(store,sumPrice,minPrice,deliveryCost,minPrice,deadline);
drawMenus(nickmenus);
},
error: function (error) {
console.error('Error:', error);
}
});
}
실행시키고 전달하는데 안받아짐?
$가 아니라 [[ 이런걸로 받아야 하는거 같음?
url: `/api/v1/test/posts/[[${postId}]]`,
이렇게 해봄!
<http://localhost:8080/api/v1/test/readpost/3>
여기로 접속하면
아예 못 찾는거 같은데?
당연하다 주소를 주석처리해둠
@GetMapping("/test/readpost/{postId}")
@RequestMapping(value = "postId", method = RequestMethod.GET)
public String readpostPage(
@PathVariable(name = "postId") Long postId
){
ModelAndView mav = new ModelAndView("postId");
mav.addObject("postId",postId);
return "domain/post/readpost";
}
이렇게 해봄
<http://localhost:8080/api/v1/test/readpost/3>
똑같이 안 됨
Failed to load resource: the server responded with a status of 403 ()
콘솔에는 이렇게 뜬다
@GetMapping("/test/readpost/{postId}")
//@RequestMapping(value = "postId", method = RequestMethod.GET)
public String readpostPage(
@PathVariable(name = "postId") Long postId
){
ModelAndView mav = new ModelAndView("postId");
mav.addObject("postId",postId);
return "domain/post/readpost";
}
이렇게 한번 해봄
[[${postId}]]
이렇게 값을 추출해봄
$.ajax({
type: 'GET',
url: `/api/v1/test/posts/[[${postId}]]`,
dataType: "json",
contentType: 'application/json',
data: {},
http://localhost:8080/api/v1/test/readpost/3
와 된다~~~
⛳해결 : ModelAndView로 전달하고 html에서 [[${postId}]] 이렇게 추출해옴
이렇게 하고 오늘은 마감함!!
오늘 한거 내일 좀 가다듬을것임
'개발일지' 카테고리의 다른 글
2024-01-17, Today I Learned (0) | 2024.01.17 |
---|---|
2024-01-16, Today I Learned (0) | 2024.01.16 |
2024-01-14, Today I Learned (0) | 2024.01.15 |
2024-01-13, Today I Learned (0) | 2024.01.14 |
2024-01-12, Today I Learned (0) | 2024.01.12 |