Skip to content

Commit 99d7a95

Browse files
authored
Merge pull request #265 from manNomi/fix/pr
Fix/pr
2 parents 7fd4991 + 9c69651 commit 99d7a95

File tree

11 files changed

+235
-14
lines changed

11 files changed

+235
-14
lines changed

.env

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ NEXT_PUBLIC_COOKIE_LOGIN_ENABLED=true
99
NEXT_PUBLIC_IMAGE_URL=https://d1q5o8tzvz4j3d.cloudfront.net
1010
NEXT_PUBLIC_UPLOADED_IMAGE_URL=https://d23lwokhcc3r0c.cloudfront.net
1111

12-
# google maps
13-
NEXT_PUBLIC_GOOGLE_MAP_API_KEY=AIzaSyAm-HhRKVeHOF6uXmf6Mgw1yJPxwnlEf0w
1412

1513
# google analytics
1614
NEXT_PUBLIC_GOOGLE_ANALYTICS_ID=G-V1KLYZC1DS
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export enum QueryKeys {
2+
posts = "posts",
3+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useRouter } from "next/navigation";
2+
3+
import { AxiosResponse } from "axios";
4+
5+
import { axiosInstance } from "@/utils/axiosInstance";
6+
7+
import { QueryKeys } from "./queryKey";
8+
9+
import { useMutation, useQueryClient } from "@tanstack/react-query";
10+
11+
/**
12+
* @description 게시글 삭제 API 응답 타입
13+
* @property {string} message - 성공 메시지
14+
* @property {number} postId - 삭제된 게시글 ID
15+
*/
16+
interface DeletePostResponse {
17+
message: string;
18+
postId: number;
19+
}
20+
21+
/**
22+
* @description postId를 받아 해당 게시글을 삭제하는 API 함수
23+
* @param postId - 삭제할 게시글의 ID
24+
* @returns Promise<AxiosResponse<DeletePostResponse>>
25+
*/
26+
export const deletePostApi = (postId: number): Promise<AxiosResponse<DeletePostResponse>> => {
27+
return axiosInstance.delete(`/posts/${postId}`);
28+
};
29+
30+
/**
31+
* @description 게시글 삭제를 위한 useMutation 커스텀 훅
32+
*/
33+
const useDeletePost = () => {
34+
const router = useRouter();
35+
const queryClient = useQueryClient();
36+
37+
return useMutation({
38+
// mutation 실행 시 호출될 함수
39+
mutationFn: deletePostApi,
40+
41+
// mutation 성공 시 실행될 콜백
42+
onSuccess: () => {
43+
// 'posts' 쿼리 키를 가진 모든 쿼리를 무효화하여
44+
// 게시글 목록을 다시 불러오도록 합니다.
45+
// ['posts', 'list'] 등 구체적인 키를 사용하셔도 좋습니다.
46+
queryClient.invalidateQueries({ queryKey: [QueryKeys.posts] });
47+
48+
alert("게시글이 성공적으로 삭제되었습니다.");
49+
50+
// 게시글 목록 페이지 이동
51+
router.replace("/community/FREE");
52+
},
53+
54+
// mutation 실패 시 실행될 콜백
55+
onError: (error) => {
56+
console.error("게시글 삭제 실패:", error);
57+
alert("게시글 삭제에 실패했습니다. 잠시 후 다시 시도해주세요.");
58+
},
59+
});
60+
};
61+
62+
export default useDeletePost;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { AxiosResponse } from "axios";
2+
3+
import { axiosInstance } from "@/utils/axiosInstance";
4+
5+
import { useMutation, useQueryClient } from "@tanstack/react-query";
6+
7+
/**
8+
* @description 위시리스트 학교 추가 API 응답 타입
9+
* @property {number} universityInfoForApplyId - 추가된 학교 정보 ID
10+
* @property {string} message - 성공 메시지
11+
*/
12+
interface UniversityFavoriteResponse {
13+
universityInfoForApplyId: number;
14+
message: string;
15+
}
16+
17+
/**
18+
* @description 위시리스트에 학교를 추가하는 API 함수
19+
* @param universityInfoForApplyId - 추가할 학교 정보의 ID
20+
* @returns Promise<AxiosResponse<UniversityFavoriteResponse>>
21+
*/
22+
export const postUniversityFavoriteApi = (
23+
universityInfoForApplyId: number,
24+
): Promise<AxiosResponse<UniversityFavoriteResponse>> =>
25+
axiosInstance.post(`/univ-apply-infos/${universityInfoForApplyId}/like`);
26+
27+
/**
28+
* @description 위시리스트 학교 추가를 위한 useMutation 커스텀 훅
29+
*/
30+
const usePostUniversityFavorite = () => {
31+
const queryClient = useQueryClient();
32+
33+
return useMutation({
34+
// mutation 실행 시 호출될 함수
35+
mutationFn: postUniversityFavoriteApi,
36+
37+
// mutation 성공 시 실행될 콜백
38+
onSuccess: () => {
39+
// 위시리스트 관련 쿼리를 무효화하여 데이터를 다시 불러옵니다.
40+
// ['favorites'], ['univ-apply-infos', 'like'] 등 구체적인 키를 사용하세요.
41+
queryClient.invalidateQueries({ queryKey: ["favorites"] });
42+
alert("위시리스트에 학교를 추가했습니다.");
43+
},
44+
45+
// mutation 실패 시 실행될 콜백
46+
onError: (error) => {
47+
const errorMessage = (error as any)?.response?.data?.message || "요청에 실패했습니다.";
48+
console.error("위시리스트 추가 실패:", errorMessage);
49+
alert(errorMessage || "요청에 실패했습니다. 잠시 후 다시 시도해주세요.");
50+
},
51+
});
52+
};
53+
54+
export default usePostUniversityFavorite;

src/app/community/[boardCode]/[postId]/CommentSection.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const CommentSection = ({ comments, postId, refresh }: CommentSectionProps) => {
4949
});
5050
};
5151

52+
console.log("comments", comments);
53+
5254
return (
5355
<div className="min-h-[50vh] pb-[49px]">
5456
{comments?.map((comment) => (
@@ -92,6 +94,8 @@ const Comment = ({
9294
setActiveDropdown(activeDropdown === commentId ? null : commentId);
9395
};
9496

97+
const isDeleted = comment.content === "";
98+
9599
return (
96100
<div
97101
className={clsx(
@@ -126,7 +130,12 @@ const Comment = ({
126130
)}
127131
<div className="flex w-full flex-col">
128132
<div className="flex justify-between">
129-
<CommentProfile user={comment.postFindSiteUserResponse} />
133+
<CommentProfile
134+
user={{
135+
...comment.postFindSiteUserResponse,
136+
nickname: isDeleted ? "알 수 없음" : comment.postFindSiteUserResponse.nickname,
137+
}}
138+
/>
130139
{comment.isOwner && (
131140
<CommentDropdown
132141
commentId={comment.id}
@@ -136,7 +145,9 @@ const Comment = ({
136145
/>
137146
)}
138147
</div>
139-
<div className="mt-3 text-sm font-normal leading-normal text-black">{comment.content}</div>
148+
<div className="mt-3 text-sm font-normal leading-normal text-black">
149+
{isDeleted ? "삭제된 댓글입니다" : comment.content}
150+
</div>
140151
<div className="mt-2 overflow-hidden text-xs font-normal leading-normal text-[#7c7c7c]">
141152
{convertISODateToDateTime(comment.createdAt) || "1970. 01. 01. 00:00"}
142153
</div>
@@ -152,14 +163,14 @@ const CommentProfile = ({ user }: { user: CommunityUser }) => {
152163
<Image
153164
className="h-full w-full rounded-full"
154165
src={
155-
user.profileImageUrl ? convertUploadedImageUrl(user.profileImageUrl) : "/images/placeholder/profile64.svg"
166+
user?.profileImageUrl ? convertUploadedImageUrl(user?.profileImageUrl) : "/images/placeholder/profile64.svg"
156167
}
157168
width={40}
158169
height={40}
159170
alt="alt"
160171
/>
161172
</div>
162-
<div className="overflow-hidden text-sm font-medium leading-normal text-black">{user.nickname}</div>
173+
<div className="overflow-hidden text-sm font-medium leading-normal text-black">{user?.nickname}</div>
163174
</div>
164175
);
165176
};

src/app/community/[boardCode]/[postId]/KebabMenu.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"use client";
22

3+
import { useRouter } from "next/navigation";
34
import { useEffect, useRef, useState } from "react";
45

56
import ReportPanel from "@/components/ui/ReportPanel";
67

8+
import useDeletePost from "@/api/community/client/useDeletePost";
79
import { IconSetting } from "@/public/svgs/mentor";
810

911
const useClickOutside = (ref, handler) => {
@@ -38,10 +40,14 @@ const IconLink = () => (
3840
// --- 메인 컴포넌트 ---
3941
type KebabMenuProps = {
4042
postId: number;
43+
boardCode: string;
44+
isOwner?: boolean;
4145
};
4246

43-
const KebabMenu = ({ postId }: KebabMenuProps) => {
47+
const KebabMenu = ({ postId, boardCode, isOwner = false }: KebabMenuProps) => {
4448
const dropdownRef = useRef<HTMLDivElement>(null);
49+
const { mutate: deletePost } = useDeletePost();
50+
const router = useRouter();
4551

4652
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
4753

@@ -77,7 +83,9 @@ const KebabMenu = ({ postId }: KebabMenuProps) => {
7783
{isDropdownOpen && (
7884
<div className="absolute right-0 top-full z-10 mt-2 w-40 origin-top-right rounded-lg border border-gray-100 bg-white shadow-lg">
7985
<ul className="p-1">
80-
<ReportPanel idx={postId} />
86+
<li>
87+
<ReportPanel idx={postId} />
88+
</li>
8189
<li key={"URL 복사"}>
8290
<button
8391
onClick={handleCopyUrl}
@@ -89,6 +97,32 @@ const KebabMenu = ({ postId }: KebabMenuProps) => {
8997
<span>{"URL 복사"}</span>
9098
</button>
9199
</li>
100+
{isOwner && (
101+
<>
102+
<li key={"수정하기"}>
103+
<button
104+
onClick={() => {
105+
router.push(`/community/${boardCode}/${postId}/modify`);
106+
}}
107+
className={`flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-sm text-gray-700 hover:bg-gray-50`}
108+
>
109+
<span>{"수정하기"}</span>
110+
</button>
111+
</li>
112+
<li key={"삭제하기"}>
113+
<button
114+
onClick={() => {
115+
if (confirm("정말로 삭제하시겠습니까?")) {
116+
deletePost(postId);
117+
}
118+
}}
119+
className={`flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-sm text-gray-700 hover:bg-gray-50`}
120+
>
121+
<span>{"삭제하기"}</span>
122+
</button>
123+
</li>
124+
</>
125+
)}
92126
</ul>
93127
</div>
94128
)}

src/app/community/[boardCode]/[postId]/PostPageContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const PostPageContent = ({ boardCode, postId }: PostPageContentProps) => {
3535
handleBack={() => {
3636
router.push(`/community/${boardCode}`);
3737
}}
38-
icon={<KebabMenu postId={postId} />}
38+
icon={<KebabMenu isOwner={post.isOwner} postId={postId} boardCode={boardCode} />}
3939
/>
4040
<Content post={post} postId={postId} />
4141
<CommentSection comments={post.postFindCommentResponses} postId={postId} refresh={refresh} />

src/app/university/[id]/InfoSection.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const InfoSection = ({
4444
</span>
4545
</div>
4646
</div>
47-
{/* 기숙사 */}
47+
{/* 자격요건 */}
4848
{detailsForApplyFold ? (
4949
<div
5050
className="flex h-[50px] items-center justify-between rounded-sm bg-k-50 px-3"
@@ -58,7 +58,7 @@ const InfoSection = ({
5858
>
5959
<div className="flex items-center gap-2.5">
6060
<DetailsForApplyIcon />
61-
<span className="text-base font-semibold text-k-900">기숙사</span>
61+
<span className="text-base font-semibold text-k-900">자격요건</span>
6262
</div>
6363
<div className="flex h-7 w-[50px] items-center justify-center rounded-full bg-k-50">
6464
<FoldIcon />
@@ -123,7 +123,7 @@ const InfoSection = ({
123123
<div className="flex items-center justify-between rounded-sm">
124124
<div className="flex items-center gap-2.5">
125125
<DetailsForAccommodationIcon />
126-
<span className="text-base font-semibold text-k-900">기숙사</span>
126+
<span className="text-base font-semibold text-k-900">자격요건</span>
127127
</div>
128128
<div className="flex h-7 w-[50px] items-center justify-center rounded-full bg-k-50">
129129
<UnFoldIcon />
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use client";
2+
3+
import usePostUniversityFavorite from "@/api/university/client/usePostUniversityFavorite";
4+
5+
const likeIcon = (
6+
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="16" viewBox="0 0 19 16" fill="none">
7+
<path
8+
d="M13.3562 0C11.7398 0 10.3052 0.634385 9.33325 1.72476C8.3613 0.634385 6.9267 0 5.31026 0C3.9024 0.00169364 2.55268 0.558508 1.55717 1.54831C0.561652 2.53811 0.0016233 3.88008 -8.01086e-05 5.27987C-8.01086e-05 11.0669 8.51337 15.6908 8.87544 15.8852C9.01614 15.9606 9.17345 16 9.33325 16C9.49306 16 9.65037 15.9606 9.79107 15.8852C10.1531 15.6908 18.6666 11.0669 18.6666 5.27987C18.6649 3.88008 18.1049 2.53811 17.1093 1.54831C16.1138 0.558508 14.7641 0.00169364 13.3562 0ZM12.9145 11.3885C11.7939 12.334 10.596 13.1849 9.33325 13.9325C8.07048 13.1849 6.87258 12.334 5.75199 11.3885C4.00843 9.90136 1.93095 7.63342 1.93095 5.27987C1.93095 4.38877 2.28699 3.53416 2.92073 2.90405C3.55447 2.27394 4.41402 1.91995 5.31026 1.91995C6.74245 1.91995 7.9413 2.67194 8.43935 3.88311C8.51184 4.05967 8.63557 4.21076 8.79478 4.31712C8.95399 4.42348 9.14145 4.48028 9.33325 4.48028C9.52506 4.48028 9.71252 4.42348 9.87173 4.31712C10.0309 4.21076 10.1547 4.05967 10.2272 3.88311C10.7252 2.67194 11.9241 1.91995 13.3562 1.91995C14.2525 1.91995 15.112 2.27394 15.7458 2.90405C16.3795 3.53416 16.7356 4.38877 16.7356 5.27987C16.7356 7.63342 14.6581 9.90136 12.9145 11.3885Z"
9+
fill="#4672EE"
10+
/>
11+
</svg>
12+
);
13+
14+
const copyIcon = (
15+
<svg xmlns="http://www.w3.org/2000/svg" width="17" height="19" viewBox="0 0 17 19" fill="none">
16+
<path
17+
d="M8.5 2.13333V11.7667M11.7143 4.4L8.5 1L5.28571 4.4M1 10.0667V15.7333C1 16.3345 1.22576 16.911 1.62763 17.3361C2.02949 17.7612 2.57454 18 3.14286 18H13.8571C14.4255 18 14.9705 17.7612 15.3724 17.3361C15.7742 16.911 16 16.3345 16 15.7333V10.0667"
18+
stroke="#4672EE"
19+
stroke-width="2"
20+
stroke-linecap="round"
21+
stroke-linejoin="round"
22+
/>
23+
</svg>
24+
);
25+
26+
interface UniversityBtnsProps {
27+
universityId: number;
28+
}
29+
const UniversityBtns = ({ universityId }: UniversityBtnsProps) => {
30+
const { mutate: postUniversityFavorite } = usePostUniversityFavorite();
31+
32+
const handleCopy = () => {
33+
alert("URL이 복사되었습니다.");
34+
navigator.clipboard.writeText(window.location.href).then(() => {});
35+
};
36+
return (
37+
<>
38+
<button
39+
onClick={() => postUniversityFavorite(universityId)}
40+
className={`/* stroke: #FFF; stroke-width: 1px; */ /* fill: linear-gradient(...) */ /* CSS의 fill은 SVG 속성이지만, 버튼 배경으로 적용합니다. */ /* backdrop-filter: blur(2px); */ /* filter: drop-shadow(...) */ /* 기타 스타일 */ rounded-full border border-white/80 bg-[linear-gradient(136deg,rgba(255,255,255,0.4)_14.87%,rgba(199,212,250,0.8)_89.1%)] p-3 drop-shadow-[2px_2px_6px_#C7D4FA] backdrop-blur-[2px] transition-transform duration-200 ease-in-out hover:scale-110 focus:outline-none focus:ring-2 focus:ring-white/50 active:scale-95`}
41+
>
42+
{likeIcon}
43+
</button>
44+
<button
45+
onClick={handleCopy}
46+
className={`/* stroke: #FFF; stroke-width: 1px; */ /* fill: linear-gradient(...) */ /* CSS의 fill은 SVG 속성이지만, 버튼 배경으로 적용합니다. */ /* backdrop-filter: blur(2px); */ /* filter: drop-shadow(...) */ /* 기타 스타일 */ rounded-full border border-white/80 bg-[linear-gradient(136deg,rgba(255,255,255,0.4)_14.87%,rgba(199,212,250,0.8)_89.1%)] p-3 drop-shadow-[2px_2px_6px_#C7D4FA] backdrop-blur-[2px] transition-transform duration-200 ease-in-out hover:scale-110 focus:outline-none focus:ring-2 focus:ring-white/50 active:scale-95`}
47+
>
48+
{copyIcon}
49+
</button>
50+
</>
51+
);
52+
};
53+
54+
export default UniversityBtns;

src/app/university/[id]/UniversityDetail.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Image from "next/image";
2+
import Link from "next/link";
23

34
import { convertImageUrl } from "@/utils/fileUtils";
45

@@ -9,6 +10,7 @@ import MajorSection from "./MajorSection";
910
import MapSection from "./MapSection";
1011
import SubTitleSection from "./SubTitleSection";
1112
import TitleSection from "./TitleSection";
13+
import UniversityBtns from "./UniversityBtns";
1214

1315
import { University } from "@/types/university";
1416

@@ -18,9 +20,12 @@ interface UniversityDetailProps {
1820
}
1921

2022
const UniversityDetail = ({ university, koreanName }: UniversityDetailProps) => {
21-
console.log("university", university);
2223
return (
2324
<div className="relative">
25+
<div className="absolute top-4 flex w-full justify-between gap-3 px-5">
26+
<UniversityBtns universityId={university.id} />
27+
</div>
28+
2429
<div className="relative -z-10 h-60 w-full bg-blue-100">
2530
<Image alt="대학 이미지" src={convertImageUrl(university.backgroundImageUrl)} fill className="object-cover" />
2631
</div>

0 commit comments

Comments
 (0)