From 82dd83f68773215364016965074f13c13e92ff9e Mon Sep 17 00:00:00 2001 From: sanghun Date: Sun, 16 Feb 2025 01:02:10 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat=20:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이력서 소프트 삭제 구현, 해당 이력서 유저만 이력서 삭제 가능 --- .../api/resume/controller/ResumeController.java | 10 ++++++++++ .../api/resume/repository/ResumeRepository.java | 2 ++ .../backend/api/resume/service/ResumeService.java | 6 ++++++ .../techeer/backend/global/success/SuccessCode.java | 1 + 4 files changed, 19 insertions(+) diff --git a/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java b/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java index 0e5248a5..8d15cde1 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/controller/ResumeController.java @@ -29,6 +29,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -141,4 +142,13 @@ public CommonResponse> searchResumesByTag(@RequestP return CommonResponse.of(SuccessCode.OK, pageableResumeResponse); } + + @Operation(summary = "이력서 삭제") + @DeleteMapping("/resumes/{resume_id}") + public CommonResponse deleteResume(@PathVariable("resume_id") Long resumeId) { + User user = userService.getLoginUser(); + + resumeService.softDeleteResume(user, resumeId); + return CommonResponse.of(SuccessCode.RESUME_SOFT_DELETED, null); + } } diff --git a/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java b/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java index 50ce68c7..4d42ec5b 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/repository/ResumeRepository.java @@ -14,6 +14,8 @@ @Repository public interface ResumeRepository extends JpaRepository, ResumeRepositoryQueryDsl { + Optional findByUserAndId(User user, Long id); + @Query("SELECT r FROM Resume r WHERE r.user.username = :username") List findResumesByUsername(@Param("username") String username); diff --git a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java index 66d4e8b7..ee312d1c 100644 --- a/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java +++ b/backend/src/main/java/com/techeer/backend/api/resume/service/ResumeService.java @@ -67,4 +67,10 @@ public Resume findLaterByUser(User user) { public Slice getResumesByUser(User user) { return resumeRepository.findResumeByUser(user); } + + public void softDeleteResume(User user, Long resumeId) { + Resume resume = resumeRepository.findByUserAndId(user, resumeId) + .orElseThrow(ResumeNotFoundException::new); + resume.softDelete(); + } } diff --git a/backend/src/main/java/com/techeer/backend/global/success/SuccessCode.java b/backend/src/main/java/com/techeer/backend/global/success/SuccessCode.java index d229862f..8c3d7865 100644 --- a/backend/src/main/java/com/techeer/backend/global/success/SuccessCode.java +++ b/backend/src/main/java/com/techeer/backend/global/success/SuccessCode.java @@ -17,6 +17,7 @@ public enum SuccessCode implements BaseStatus { // Resume Success RESUME_CREATED(HttpStatus.CREATED, "RESUME_201", "이력서가 성공적으로 저장되었습니다"), RESUME_FETCH_OK(HttpStatus.OK, "RESUME_200", "이력서 조회 성공"), + RESUME_SOFT_DELETED(HttpStatus.NO_CONTENT, "RESUME_204", "이력서 소프트 삭제 성공"), // User Success USER_FETCH_OK(HttpStatus.OK, "USER_200", "유저 정보 조회 성공"), From 771dca58ccdf48e8b8f8b16bacc5b1d08d1ae447 Mon Sep 17 00:00:00 2001 From: sanghun Date: Mon, 17 Feb 2025 22:44:01 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix=20:=20=EC=82=AC=EC=9A=A9=EB=90=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=BD=94=EB=93=9C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/api/user/converter/UserConverter.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/backend/src/main/java/com/techeer/backend/api/user/converter/UserConverter.java b/backend/src/main/java/com/techeer/backend/api/user/converter/UserConverter.java index a2e9da7f..f612dd19 100644 --- a/backend/src/main/java/com/techeer/backend/api/user/converter/UserConverter.java +++ b/backend/src/main/java/com/techeer/backend/api/user/converter/UserConverter.java @@ -1,21 +1,9 @@ package com.techeer.backend.api.user.converter; -import com.techeer.backend.api.user.domain.Role; -import com.techeer.backend.api.user.domain.SocialType; import com.techeer.backend.api.user.domain.User; import com.techeer.backend.api.user.dto.response.UserInfoResponse; public class UserConverter { - public static User fromSignUp(String email, String username, String refreshToken, Role role, - SocialType socialType) { - return User.builder() - .email(email) - .username(username) - .refreshToken(refreshToken) - .role(role) - .socialType(socialType) - .build(); - } public static UserInfoResponse ofUserInfoResponse(User user) { return UserInfoResponse.builder() From 5daf2296567423666895471ff1337b440195c936 Mon Sep 17 00:00:00 2001 From: sanghun Date: Mon, 17 Feb 2025 22:45:14 +0900 Subject: [PATCH 3/5] =?UTF-8?q?test=20:=20=EC=9D=B4=EB=A0=A5=EC=84=9C=20?= =?UTF-8?q?=EC=86=8C=ED=94=84=ED=8A=B8=EC=82=AD=EC=A0=9C=20=EC=9C=A0?= =?UTF-8?q?=EB=8B=9B=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entity 생성을 매번 해야하는 번거로움을 없애기 위해 util 메서드 구현. 이후 이력서 소프트 삭제 유닛 테스트 코드 작성 --- .../api/resume/service/ResumeServiceTest.java | 24 ++++++++++++++++++- .../backend/util/domain/ResumeUtils.java | 16 +++++++++++++ .../backend/util/domain/UserUtils.java | 17 +++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 backend/src/test/java/com/techeer/backend/util/domain/ResumeUtils.java create mode 100644 backend/src/test/java/com/techeer/backend/util/domain/UserUtils.java diff --git a/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceTest.java b/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceTest.java index b01757aa..91b37eba 100644 --- a/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceTest.java +++ b/backend/src/test/java/com/techeer/backend/api/resume/service/ResumeServiceTest.java @@ -13,6 +13,9 @@ import com.techeer.backend.api.resume.domain.Resume; import com.techeer.backend.api.resume.exception.ResumeNotFoundException; import com.techeer.backend.api.resume.repository.ResumeRepository; +import com.techeer.backend.api.user.domain.User; +import com.techeer.backend.util.domain.ResumeUtils; +import com.techeer.backend.util.domain.UserUtils; import java.util.Arrays; import java.util.List; import java.util.Optional; @@ -29,7 +32,7 @@ class ResumeServiceTest { @Mock private ResumeRepository resumeRepository; - + @BeforeEach void setUp() { @@ -122,4 +125,23 @@ void searchResumesByUserName_ShouldReturnListOfResumes1() { verify(resumeRepository, times(1)).findResumesByUsername(userName); } + + @Test + void deleteResume_WhenResumeExists_ShouldDeleteResume() { + // Given (테스트 준비) + User user = UserUtils.newInstance(); + Resume resume = ResumeUtils.newInstance(); + Long resumeId = 1L; + + // Mocking: 특정 user와 resumeId로 조회하면 해당 resume 반환 + when(resumeRepository.findByUserAndId(user, resumeId)).thenReturn(Optional.of(resume)); + + // When (테스트 실행) + resumeService.softDeleteResume(user, resumeId); + + // Then (결과 검증) + assertNotNull(resume.getDeletedAt()); // deletedAt이 null이 아니면 softDelete()가 정상 동작한 것 + verify(resumeRepository, times(1)).findByUserAndId(user, resumeId); + } + } diff --git a/backend/src/test/java/com/techeer/backend/util/domain/ResumeUtils.java b/backend/src/test/java/com/techeer/backend/util/domain/ResumeUtils.java new file mode 100644 index 00000000..088a9cb0 --- /dev/null +++ b/backend/src/test/java/com/techeer/backend/util/domain/ResumeUtils.java @@ -0,0 +1,16 @@ +package com.techeer.backend.util.domain; + +import com.techeer.backend.api.resume.domain.Resume; +import com.techeer.backend.api.tag.position.Position; + +public class ResumeUtils { + public static Resume newInstance() { + return Resume.builder() + .user(UserUtils.newInstance()) // 연관된 User 객체 추가 + .name("test_resume") + .career(3) // 경력 (예: 3년) + .position(Position.BACKEND) // 실제 Position Enum 값을 확인 후 넣어주세요 + .resumePdf(null) // ResumePdf를 넣을 경우 적절한 객체를 생성해야 함 + .build(); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/techeer/backend/util/domain/UserUtils.java b/backend/src/test/java/com/techeer/backend/util/domain/UserUtils.java new file mode 100644 index 00000000..21da853c --- /dev/null +++ b/backend/src/test/java/com/techeer/backend/util/domain/UserUtils.java @@ -0,0 +1,17 @@ +package com.techeer.backend.util.domain; + +import com.techeer.backend.api.user.domain.Role; +import com.techeer.backend.api.user.domain.SocialType; +import com.techeer.backend.api.user.domain.User; + +public class UserUtils { + public static User newInstance() { + return User.builder() + .email("test_user@example.com") + .username("test_man") + .refreshToken("test_refresh_token") + .role(Role.REGULAR) // 실제 Role Enum 값을 확인 후 넣어주세요 + .socialType(SocialType.GOOGLE) // 실제 SocialType Enum 값을 확인 후 넣어주세요 + .build(); + } +} From 83f7d57d8bc14a465f5bb3e453ed0ccbffd1b1ca Mon Sep 17 00:00:00 2001 From: jung2941 Date: Mon, 17 Feb 2025 14:51:04 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 2 +- frontend/src/api/bookMarkApi.ts | 1 - .../components/MyInfoPage/BookmarkItem.tsx | 6 + .../src/components/MyInfoPage/BookmarkTap.tsx | 6 +- frontend/src/pages/ResumeFeedbackPage.tsx | 203 ++++++++---------- frontend/src/store/BookmarkStore.ts | 15 ++ 6 files changed, 119 insertions(+), 114 deletions(-) create mode 100644 frontend/src/store/BookmarkStore.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d97d5ddb..8bc4622f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,9 +21,9 @@ export default function App() { } /> } /> } /> + } /> }> - } /> } /> } /> diff --git a/frontend/src/api/bookMarkApi.ts b/frontend/src/api/bookMarkApi.ts index 46d220b5..5d742192 100644 --- a/frontend/src/api/bookMarkApi.ts +++ b/frontend/src/api/bookMarkApi.ts @@ -5,7 +5,6 @@ export const postBookmark = async (resumeId: number) => { try { console.log("아이디", resumeId); const response = await jsonAxios.post(`/bookmarks/${resumeId}`); - // 응답에서 bookmark_id를 받아옵니다 const bookmarkId = response.data.result.bookmark_id; return bookmarkId; } catch (error) { diff --git a/frontend/src/components/MyInfoPage/BookmarkItem.tsx b/frontend/src/components/MyInfoPage/BookmarkItem.tsx index 9d89cb13..70351a9d 100644 --- a/frontend/src/components/MyInfoPage/BookmarkItem.tsx +++ b/frontend/src/components/MyInfoPage/BookmarkItem.tsx @@ -3,6 +3,8 @@ import { Bookmark, Trash } from "lucide-react"; import Swal from "sweetalert2"; import { BookmarkType } from "../../dataType"; import { deleteBookmarkById } from "../../api/bookMarkApi"; +import { useBookmarkStore } from "../../store/BookmarkStore.ts"; +import { getBookmarkById } from "../../api/bookMarkApi.ts"; type BookmarkItemProps = { bookmark: BookmarkType; @@ -11,6 +13,8 @@ type BookmarkItemProps = { function BookmarkItem({ bookmark, onUpdate }: BookmarkItemProps) { const navigate = useNavigate(); + const { setBookmarks } = useBookmarkStore(); + const userId = 1; // 임시. 지금 userId 필요 없음 const handleBookmarkClick = () => { navigate(`/feedback/${bookmark.resume_id}`); @@ -36,6 +40,8 @@ function BookmarkItem({ bookmark, onUpdate }: BookmarkItemProps) { "success" ); onUpdate(); + const response = await getBookmarkById(userId); + setBookmarks(response.result); } catch (error) { console.error("북마크 삭제 오류:", error); Swal.fire("오류", "북마크를 제거하는 데 실패했습니다.", "error"); diff --git a/frontend/src/components/MyInfoPage/BookmarkTap.tsx b/frontend/src/components/MyInfoPage/BookmarkTap.tsx index c0a600aa..3933b28e 100644 --- a/frontend/src/components/MyInfoPage/BookmarkTap.tsx +++ b/frontend/src/components/MyInfoPage/BookmarkTap.tsx @@ -1,10 +1,10 @@ -import { useState, useEffect } from "react"; +import { useEffect } from "react"; import BookmarkItem from "../../components/MyInfoPage/BookmarkItem.tsx"; import { getBookmarkById } from "../../api/bookMarkApi.ts"; -import { BookmarkType } from "../../dataType.ts"; +import { useBookmarkStore } from "../../store/BookmarkStore.ts"; function BookmarkTap() { - const [bookmarks, setBookmarks] = useState([]); + const { bookmarks, setBookmarks } = useBookmarkStore(); const userId = 1; // 임시. 지금 userId 필요 없음 diff --git a/frontend/src/pages/ResumeFeedbackPage.tsx b/frontend/src/pages/ResumeFeedbackPage.tsx index 5b3a4ae1..28b07a2f 100644 --- a/frontend/src/pages/ResumeFeedbackPage.tsx +++ b/frontend/src/pages/ResumeFeedbackPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; import Layout from "../components/Layout/Layout"; import MainContainer from "../components/resumeoverview/MainContainer"; import ResumeOverview from "../components/resumeoverview/ResumeOverview"; @@ -11,95 +12,109 @@ import { getResumeApi, postAiFeedback, } from "../api/feedbackApi.ts"; +import { + postBookmark, + deleteBookmarkById, + getBookmarkById, +} from "../api/bookMarkApi.ts"; import { AddFeedbackPoint, FeedbackPoint, ResumeData } from "../types.ts"; import useResumeStore from "../store/ResumeStore.ts"; -import { useParams } from "react-router-dom"; +import { useBookmarkStore } from "../store/BookmarkStore.ts"; import { Bookmark, BookmarkMinus } from "lucide-react"; -import { postBookmark, deleteBookmarkById } from "../api/bookMarkApi.ts"; +import Swal from "sweetalert2"; function ResumeFeedbackPage() { - const [resumeData, setResumeData] = useState(null); //이력서 데이터를 관리 - const [feedbackPoints, setFeedbackPoints] = useState([]); //피드백 데이터를 관리 - const [hoveredCommentId, setHoveredCommentId] = useState(null); //마우스 호버시 코멘트 아이디를 관리 - const [loading, setLoading] = useState(true); //로딩 상태를 관리 - const [error, setError] = useState(null); //에러 상태를 관리 - const { setResumeUrl } = useResumeStore(); //이력서 아이디와 이력서 URL을 관리 -> 이게 왜 필요한지 모르겠음 - const { id } = useParams(); // useParams로 URL의 id 파라미터 가져오기 - const [isBookmarked, setIsBookmarked] = useState(false); // 북마크 상태 - const [bookmarkId, setBookmarkId] = useState(null); // 북마크 ID 저장 + const [resumeData, setResumeData] = useState(null); + const [feedbackPoints, setFeedbackPoints] = useState([]); + const [hoveredCommentId, setHoveredCommentId] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { id } = useParams(); + const resumeId = Number(id); + const { setResumeUrl } = useResumeStore(); + const { bookmarks, setBookmarks, isBookmarked } = useBookmarkStore(); useEffect(() => { const fetchData = async () => { - console.log("api호출"); - if (!id) { + if (!resumeId) { setError("Resume ID is missing."); return; } - try { setLoading(true); setError(null); - const data = await getResumeApi(Number(id)); //여기서 레쥬메 아이디로 관리를 해서 데이터를 불러옴 + + const data = await getResumeApi(resumeId); setResumeData(data); - // 북마크 상태 초기화 - const bookmarkResponse = await postBookmark(Number(id)); // userId 제거, resumeId만 사용 - if (bookmarkResponse) { - setIsBookmarked(true); - setBookmarkId(bookmarkResponse); - } + setFeedbackPoints(data.feedbackResponses || []); setResumeUrl(data.fileUrl); - console.log("id:", id); - setFeedbackPoints(data.feedbackResponses || []); // 관련된 설정들 + + const userId = 1; + const bookmarksData = await getBookmarkById(userId); + setBookmarks(bookmarksData.result || []); } catch (error) { - console.error("Failed to fetch resume data", error); - setError("Failed to fetch resume data. Please try again later."); + console.error(error); } finally { setLoading(false); } }; - fetchData(); - }, [id]); + }, [resumeId, setResumeUrl, setBookmarks]); - // 북마크 토글 기능 const toggleBookmark = async () => { - if (!id) { + if (!resumeId) { setError("Resume ID is missing."); return; } try { - if (isBookmarked) { - // 북마크 해제 - if (bookmarkId !== null) { - await deleteBookmarkById(bookmarkId); - } - setIsBookmarked(false); - setBookmarkId(null); + const existingBookmark = bookmarks.find( + (bk) => bk.resume_id === resumeId + ); + + if (existingBookmark) { + await deleteBookmarkById(existingBookmark.bookmark_id); + setBookmarks( + bookmarks.filter( + (bk) => bk.bookmark_id !== existingBookmark.bookmark_id + ) + ); + Swal.fire({ + icon: "success", + title: "북마크가 해제되었습니다.", + confirmButtonText: "확인", + }); } else { - // 북마크 추가 - const response = await postBookmark(Number(id)); // userId 제거, resumeId만 사용 - console.log("포스트북마크 값", response); - setIsBookmarked(true); - setBookmarkId(response); // 서버로부터 받은 북마크 ID 저장 - console.log("북마크 아이디 생성", response); + const newBookmarkId = await postBookmark(resumeId); + setBookmarks([ + ...bookmarks, + { + bookmark_id: newBookmarkId, + resume_id: resumeId, + //임시 + }, + ]); + Swal.fire({ + icon: "success", + title: "북마크가 추가되었습니다.", + confirmButtonText: "확인", + }); } - } catch (error) { - console.error("Failed to toggle bookmark", error); - alert("북마크 상태를 변경할 수 없습니다. 다시 시도해주세요."); + } catch { + Swal.fire({ + icon: "error", + title: "북마크 상태를 변경할 수 없습니다. 다시 시도해주세요.", + confirmButtonText: "확인", + }); } }; const handleAiFeedback = async () => { setLoading(true); try { - const aiFeedback = await postAiFeedback(Number(id)); - setFeedbackPoints((prevPoints) => [ - ...prevPoints, - ...aiFeedback.feedbacks, - ]); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (error) { + const aiFeedback = await postAiFeedback(resumeId); + setFeedbackPoints((prev) => [...prev, ...aiFeedback.feedbacks]); + } catch { setError("Failed to retrieve AI feedback."); } finally { setLoading(false); @@ -107,50 +122,35 @@ function ResumeFeedbackPage() { }; const addFeedbackPoint = async (point: Omit) => { - if ( - !point.content || - point.xCoordinate === undefined || - point.yCoordinate === undefined - ) { - setError("All fields are required to add a feedback point."); - return; - } - try { + if ( + !point.content || + point.xCoordinate === undefined || + point.yCoordinate === undefined + ) { + setError("All fields are required to add a feedback point."); + return; + } setLoading(true); - const newPoint: AddFeedbackPoint = { - xCoordinate: point.xCoordinate, - yCoordinate: point.yCoordinate, - content: point.content, - pageNumber: 1, - }; - await addFeedbackApi(Number(id), newPoint); - const updatedData = await getResumeApi(Number(id)); - console.log("업데이트 데이터: ", updatedData); + const newPoint: AddFeedbackPoint = { ...point, pageNumber: 1 }; + await addFeedbackApi(resumeId, newPoint); + const updatedData = await getResumeApi(resumeId); setFeedbackPoints(updatedData.feedbackResponses); - } catch (error) { - console.error("Failed to add feedback point", error); + } catch { setError("Failed to add feedback point. Please try again later."); } finally { setLoading(false); } }; - const deleteFeedbackPoint = async (id: number) => { + const deleteFeedbackPoint = async (feedbackId: number) => { try { setLoading(true); - setError(null); - - // 피드백 점 삭제 API 호출 - await deleteFeedbackApi(Number(id), id); - - // 삭제 후 상태 갱신 - setFeedbackPoints((prevComments) => - (prevComments || []).filter((item) => item.id !== id) + await deleteFeedbackApi(resumeId, feedbackId); + setFeedbackPoints((prev) => + prev.filter((item) => item.id !== feedbackId) ); - console.log("Deleted feedback point: ", id); - } catch (error) { - console.error("Failed to delete feedback point", error); + } catch { setError("Failed to delete feedback point. Please try again later."); } finally { setLoading(false); @@ -161,50 +161,37 @@ function ResumeFeedbackPage() { console.log("Edit feedback point: ", updatedItem); }; - if (loading) { - return ; - } - - if (error) { - return ; - } + if (loading) return ; + if (error) return ; + if (!resumeData) return
No resume data available.
; - if (!resumeData) { - return
No resume data available.
; - } + const bookmarked = isBookmarked(resumeId); return (
- {/* 북마크 버튼 */} - - {/* Resume Overview */} - - {/* Comment Section */}
void; + isBookmarked: (resumeId: number) => boolean; +} + +export const useBookmarkStore = create((set, get) => ({ + bookmarks: [], + setBookmarks: (bookmarks) => set({ bookmarks }), + isBookmarked: (resumeId) => + get().bookmarks.some((b) => b.resume_id === resumeId), +})); From 85b17bb5806188150721d59b4f49361591538665 Mon Sep 17 00:00:00 2001 From: jung2941 Date: Mon, 17 Feb 2025 16:22:46 +0900 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=EB=82=A0=EC=A7=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/bookMarkApi.ts | 4 +--- .../components/MyInfoPage/BookmarkItem.tsx | 19 ++++++++----------- .../src/components/MyInfoPage/BookmarkTap.tsx | 9 ++------- frontend/src/dataType.ts | 8 ++++++++ frontend/src/pages/ResumeFeedbackPage.tsx | 11 ++--------- frontend/src/store/BookmarkStore.ts | 6 +++--- frontend/src/utils/DateFormatter.ts | 8 ++++++++ 7 files changed, 32 insertions(+), 33 deletions(-) create mode 100644 frontend/src/utils/DateFormatter.ts diff --git a/frontend/src/api/bookMarkApi.ts b/frontend/src/api/bookMarkApi.ts index 5d742192..6856de38 100644 --- a/frontend/src/api/bookMarkApi.ts +++ b/frontend/src/api/bookMarkApi.ts @@ -3,10 +3,8 @@ import { jsonAxios } from "./axios.config.ts"; // 북마크 추가 export const postBookmark = async (resumeId: number) => { try { - console.log("아이디", resumeId); const response = await jsonAxios.post(`/bookmarks/${resumeId}`); - const bookmarkId = response.data.result.bookmark_id; - return bookmarkId; + return response.data.result; } catch (error) { console.error("북마크 추가 오류:", error); throw error; diff --git a/frontend/src/components/MyInfoPage/BookmarkItem.tsx b/frontend/src/components/MyInfoPage/BookmarkItem.tsx index 70351a9d..4b9c9f60 100644 --- a/frontend/src/components/MyInfoPage/BookmarkItem.tsx +++ b/frontend/src/components/MyInfoPage/BookmarkItem.tsx @@ -1,20 +1,17 @@ import { useNavigate } from "react-router-dom"; import { Bookmark, Trash } from "lucide-react"; import Swal from "sweetalert2"; -import { BookmarkType } from "../../dataType"; +import { BookmarkListType } from "../../dataType"; import { deleteBookmarkById } from "../../api/bookMarkApi"; -import { useBookmarkStore } from "../../store/BookmarkStore.ts"; -import { getBookmarkById } from "../../api/bookMarkApi.ts"; +import { formatDate } from "../../utils/DateFormatter.ts"; type BookmarkItemProps = { - bookmark: BookmarkType; + bookmark: BookmarkListType; onUpdate: () => void; // 북마크 변경 시 호출되는 콜백 }; function BookmarkItem({ bookmark, onUpdate }: BookmarkItemProps) { const navigate = useNavigate(); - const { setBookmarks } = useBookmarkStore(); - const userId = 1; // 임시. 지금 userId 필요 없음 const handleBookmarkClick = () => { navigate(`/feedback/${bookmark.resume_id}`); @@ -40,8 +37,6 @@ function BookmarkItem({ bookmark, onUpdate }: BookmarkItemProps) { "success" ); onUpdate(); - const response = await getBookmarkById(userId); - setBookmarks(response.result); } catch (error) { console.error("북마크 삭제 오류:", error); Swal.fire("오류", "북마크를 제거하는 데 실패했습니다.", "error"); @@ -55,14 +50,16 @@ function BookmarkItem({ bookmark, onUpdate }: BookmarkItemProps) {
-

2024 테커 상반기 채용 이력서

-

정유진

+

{bookmark.resume_title}

+

{bookmark.resume_author}

- 2025-01-11 + + {formatDate(bookmark.created_at)} +
{ - fetchBookmarks(); - }; - useEffect(() => { fetchBookmarks(); }, []); @@ -41,7 +36,7 @@ function BookmarkTap() { )) ) : ( diff --git a/frontend/src/dataType.ts b/frontend/src/dataType.ts index 1198722f..5a3fe2d3 100644 --- a/frontend/src/dataType.ts +++ b/frontend/src/dataType.ts @@ -23,3 +23,11 @@ export type BookmarkType = { title: string; date: string; }; + +export type BookmarkListType = { + bookmark_id: number; + created_at: string; + resume_author: string; + resume_id: number; + resume_title: string; +}; diff --git a/frontend/src/pages/ResumeFeedbackPage.tsx b/frontend/src/pages/ResumeFeedbackPage.tsx index 28b07a2f..9eae2d77 100644 --- a/frontend/src/pages/ResumeFeedbackPage.tsx +++ b/frontend/src/pages/ResumeFeedbackPage.tsx @@ -85,15 +85,8 @@ function ResumeFeedbackPage() { confirmButtonText: "확인", }); } else { - const newBookmarkId = await postBookmark(resumeId); - setBookmarks([ - ...bookmarks, - { - bookmark_id: newBookmarkId, - resume_id: resumeId, - //임시 - }, - ]); + const newBookmark = await postBookmark(resumeId); + setBookmarks([...bookmarks, newBookmark]); Swal.fire({ icon: "success", title: "북마크가 추가되었습니다.", diff --git a/frontend/src/store/BookmarkStore.ts b/frontend/src/store/BookmarkStore.ts index e237336a..99cd4163 100644 --- a/frontend/src/store/BookmarkStore.ts +++ b/frontend/src/store/BookmarkStore.ts @@ -1,9 +1,9 @@ import { create } from "zustand"; -import { BookmarkType } from "../dataType"; +import { BookmarkListType } from "../dataType"; interface BookmarkStore { - bookmarks: BookmarkType[]; - setBookmarks: (bookmarks: BookmarkType[]) => void; + bookmarks: BookmarkListType[]; + setBookmarks: (bookmarks: BookmarkListType[]) => void; isBookmarked: (resumeId: number) => boolean; } diff --git a/frontend/src/utils/DateFormatter.ts b/frontend/src/utils/DateFormatter.ts new file mode 100644 index 00000000..f0ed04cc --- /dev/null +++ b/frontend/src/utils/DateFormatter.ts @@ -0,0 +1,8 @@ +export const formatDate = (isoString: string): string => { + const date = new Date(isoString); + return date.toLocaleDateString("ko-KR", { + year: "numeric", + month: "long", + day: "numeric", + }); +};