diff --git a/src/app/api/project/projectApi.js b/src/app/api/project/projectApi.js index d47dff0..f9a13ed 100644 --- a/src/app/api/project/projectApi.js +++ b/src/app/api/project/projectApi.js @@ -97,3 +97,14 @@ export const deleteProject = async (projectId) => { } }; +// 스프린트별 기여도 랭킹 조회 +export const fetchSprintContributions = async (sprintId) => { + try { + const res = await axiosWithAuthorization.get(`/contributions/${sprintId}`); + console.log("스프린트별 기여도 랭킹 조회:", res.data); + return res.data.data; + } catch (error) { + const message = error?.response?.data?.data?.message ?? "스프린트별 기여도 랭킹 조회를 조회할 수 없습니다."; + throw new Error(message); + } +}; diff --git a/src/app/api/user/userApi.js b/src/app/api/user/userApi.js index 90bd0b5..e9ca06f 100644 --- a/src/app/api/user/userApi.js +++ b/src/app/api/user/userApi.js @@ -10,4 +10,19 @@ export const fetchUserData = async () => { const message = error?.response?.data?.data?.message ?? "회원 정보를 조회할 수 없습니다."; throw new Error(message); } +}; + + +// [개인 페이지] 개별 회원 기여도 점수 조회 +export const fetchUserContributionData = async (projectId, sprintId) => { + try { + const res = await axiosWithAuthorization.get(`/contributions/${projectId}/me`, { + params: { sprintId }, + }); + console.log("[개인 페이지] 개별 회원 기여도 점수 조회:", res.data); + return res.data.data; + } catch (error) { + const message = error?.response?.data?.data?.message ?? "[개인 페이지] 개별 회원 기여도 점수를 조회할 수 없습니다."; + throw new Error(message); + } }; \ No newline at end of file diff --git a/src/app/home/home.js b/src/app/home/home.js index 127f69f..ebb4b92 100644 --- a/src/app/home/home.js +++ b/src/app/home/home.js @@ -197,7 +197,12 @@ export default function Home() { }, [selectedId]); - // 팀 설명 15글자 이상이면 축약 표시 + // 팀 이름 4글자 이상이면 축약 표시 + const reduceName = (name) => { + return name.length > 4 ? name.slice(0, 4) + ".." : name; +}; + + // 팀 설명 10글자 이상이면 축약 표시 const reduceDescription = (name) => { return name.length > 10 ? name.slice(0, 10) + ".." : name; }; @@ -276,7 +281,7 @@ export default function Home() { window.location.href = `/team/${team.teamId}`; }}> - {team.teamName} + {reduceName(team.teamName)} { e.stopPropagation(); diff --git a/src/app/mypage/mypage.js b/src/app/mypage/mypage.js index ec8d52a..c228e13 100644 --- a/src/app/mypage/mypage.js +++ b/src/app/mypage/mypage.js @@ -14,6 +14,7 @@ import LeaderModal from "./leader_modal"; import { fetchProjectData, fetchProjectUser } from "@/app/api/project/projectApi"; import { fetchMySprintTaskData } from "@/app/api/sprint/sprintApi"; import { completeTask, sosTask } from "@/app/api/task/taskApi"; +import { fetchUserContributionData } from "@/app/api/user/userApi" import { useAlert } from "@/context/AlertContext"; @@ -35,6 +36,8 @@ export default function My({ projectId }) { const [currentSprint, setCurrentSprint] = useState(null); const [currentSprintIndex, setCurrentSprintIndex] = useState(0); + const [myContributionScore, setMyContributionScore] = useState(null); + const [clickCountMap, setClickCountMap] = useState({}); useEffect(() => { @@ -184,6 +187,21 @@ export default function My({ projectId }) { } }; + useEffect(() => { + const loadMyContribution = async () => { + if (!ProjectId || !currentSprint?.id) return; + + try { + const data = await fetchUserContributionData(ProjectId, currentSprint.id); + setMyContributionScore(data?.score ?? 0); + } catch (error) { + setMyContributionScore(0); // 실패 시 0으로 대체 + } + }; + + loadMyContribution(); + }, [ProjectId, currentSprint?.id]); + // 모달 상태에 따라 스크롤 제어 useEffect(() => { const disableScroll = () => { @@ -216,7 +234,7 @@ export default function My({ projectId }) { - {projectUserData?.projectNickname} 님의 마이페이지 + {projectUserData?.nickname} 님의 마이페이지
@@ -268,7 +286,7 @@ export default function My({ projectId }) { 기여도 {typeof currentSprint?.progress === "number" && !isNaN(currentSprint.progress) && ( - + )} @@ -277,7 +295,7 @@ export default function My({ projectId }) { 역할 - {projectUserData?.data?.role === "ADMIN" ? "팀장" : "팀원"} + {projectUserData?.role === "ADMIN" ? "팀장" : "팀원"} diff --git a/src/app/project/meeting_modal.js b/src/app/project/meeting_modal.js index 39a5044..0fd170c 100644 --- a/src/app/project/meeting_modal.js +++ b/src/app/project/meeting_modal.js @@ -29,7 +29,8 @@ export default function MeetingModal({ setStartTime, endTime, setEndTime, - sprintId, + sprintId, + sprintTitle, isEditing, onMeetingSaved, onMeetingDeleted @@ -116,7 +117,7 @@ export default function MeetingModal({ return ( e.stopPropagation()}> - Sprint {sprintId} 팀 미팅 + Sprint {sprintTitle} 팀 미팅 @@ -130,6 +131,8 @@ export default function MeetingModal({ setMeetingTitle(e.target.value); setIsTitleChanged(true); }} + maxLength={15} + placeholder="회의 명을 입력하세요 (15자 이내)" /> {/* 날짜 입력 */} diff --git a/src/app/project/project.js b/src/app/project/project.js index df3acd9..964cda0 100644 --- a/src/app/project/project.js +++ b/src/app/project/project.js @@ -16,7 +16,7 @@ import SprintModal from "./sprint_modal"; import MeetingModal from "./meeting_modal"; import ProjectModal from "../team/project_modal"; -import { fetchProjectData, fetchProjectUser, fetchProjectMemberList } from "@/app/api/project/projectApi"; +import { fetchProjectData, fetchProjectUser, fetchProjectMemberList, fetchSprintContributions } from "@/app/api/project/projectApi"; import { fetchSprintTaskData } from "@/app/api/sprint/sprintApi"; import Image from "next/image"; @@ -30,80 +30,6 @@ import { useAlert } from "@/context/AlertContext"; "#27AE60", "#D35400" ]; -const sprintContributionData = [ - { - "success": true, - "status": 200, - "data": [ - { - "contributionId": 3, - "sprintId": 1, - "memberId": 4, - "nickname": "최현태", - "profileImageUrl": "https://img1.kakaocdn.net/thumb/R110x110.q70/?fname=https%3A%2F%2Ft1.kakaocdn.net%2Faccount_images%2Fdefault_profile.jpeg", - "score": 54 - }, - { - "contributionId": 1, - "sprintId": 1, - "memberId": 2, - "nickname": "조수빈", - "profileImageUrl": "https://img1.kakaocdn.net/thumb/R110x110.q70/?fname=https%3A%2F%2Ft1.kakaocdn.net%2Faccount_images%2Fdefault_profile.jpeg", - "score": 47 - }, - { - "contributionId": 2, - "sprintId": 1, - "memberId": 3, - "nickname": null, - "profileImageUrl": null, - "score": 19 - } - ], - "timestamp": "2025-03-20T21:45:31.228694" - }, - { - "success": true, - "status": 200, - "data": [ - { - "contributionId": 3, - "sprintId": 2, - "memberId": 4, - "nickname": "최현태", - "profileImageUrl": "https://img1.kakaocdn.net/thumb/R110x110.q70/?fname=https%3A%2F%2Ft1.kakaocdn.net%2Faccount_images%2Fdefault_profile.jpeg", - "score": 54 - }, - { - "contributionId": 1, - "sprintId": 2, - "memberId": 2, - "nickname": "조수빈", - "profileImageUrl": "https://img1.kakaocdn.net/thumb/R110x110.q70/?fname=https%3A%2F%2Ft1.kakaocdn.net%2Faccount_images%2Fdefault_profile.jpeg", - "score": 47 - }, - { - "contributionId": 2, - "sprintId": 2, - "memberId": 3, - "nickname": "채민주", - "profileImageUrl": 10, - "score": 19 - }, - { - "contributionId": 2, - "sprintId": 2, - "memberId": 1, - "nickname": "정선우", - "profileImageUrl": 30, - "score": 19 - }, - ], - "timestamp": "2025-03-20T21:45:31.228694" - } -] - - export default function Project({projectId}) { const { showAlert } = useAlert(); @@ -131,6 +57,9 @@ export default function Project({projectId}) { const [hasPrev, setHasPrev] = useState(false); // 이전 스프린트 존재 여부 const [hasNext, setHasNext] = useState(false); // 다음 스프린트 존재 여부 + // 스프린트별 기여도 랭킹 조회 + const [sprintContributions, setSprintContributions] = useState({}); + //meeting const [hasMoreMeetings, setHasMoreMeetings] = useState(false); const [lastMeetingtId, setLastMeetingId] = useState(null); @@ -324,22 +253,20 @@ export default function Project({projectId}) { return enableScroll; }, [isSprintModalOpen, isCreateSprintModalOpen, isMeetingModalOpen, isProjectEditModalOpen]); - // <----------------프로젝트 멤버별 기여도 ---------------------> - const mergeProjectMembersWithContributions = (projectMembers, contributionData) => { return projectMembers.map((member, index) => { const contribution = contributionData.find( - c => c.memberId === member.projectParticipantId + c => c.projectParticipantId === member.projectParticipantId ); const score = contribution?.score ?? 0; - const nickname = (!member.nickname || member.nickname === "UNKNOWN_PROJECT_NICKNAME" || member.status === "INACTIVE" ) + const nickname = (!member.nickname || member.nickname === "UNKNOWN_PROJECT_NICKNAME" || member.status === "INACTIVE") ? "알수없음" : member.nickname; const profileImage = (!member.profileImageUrl || member.profileImageUrl === "UNKNOWN_PROJECT_PROFILE_URL" || member.status === "INACTIVE") ? "/img/default_profile.png" : member.profileImageUrl; - + return { id: member.projectParticipantId, name: nickname, @@ -355,37 +282,29 @@ export default function Project({projectId}) { const processedSprintData = useMemo(() => { if (!Array.isArray(sprintData)) return []; - - const result = sprintData.map((sprintContent, idx) => { + + return sprintData.map((sprintContent, idx) => { if (!sprintContent) return null; - - const sprintContributionsForSprint = sprintContributionData.find(item => - item.data?.[0]?.sprintId === sprintContent.id - )?.data ?? []; - const isLast = idx === sprintData.length - 1; - - return { - sprint_id: sprintContent.id, - sprint_title: sprintContent.title, - goal: sprintContent.goal, - sprint_start: sprintContent.startDt, - sprint_end: sprintContent.dueDt, - progress: sprintContent.progress ?? 0, - last: isLast, - title: sprintContent.title, - startDt: sprintContent.startDt, - dueDt: sprintContent.dueDt, - member: mergeProjectMembersWithContributions( - projectMembers, - sprintContributionsForSprint - ), - }; - }) - .filter(Boolean); - - return result; - }, [sprintData, projectMembers]); + const contributions = sprintContributions[sprintContent.id] ?? []; + + const isLast = idx === sprintData.length - 1; + + return { + sprint_id: sprintContent.id, + sprint_title: sprintContent.title, + goal: sprintContent.goal, + sprint_start: sprintContent.startDt, + sprint_end: sprintContent.dueDt, + progress: sprintContent.progress ?? 0, + last: isLast, + title: sprintContent.title, + startDt: sprintContent.startDt, + dueDt: sprintContent.dueDt, + member: mergeProjectMembersWithContributions(projectMembers, contributions), + }; + }).filter(Boolean); + }, [sprintData, projectMembers, sprintContributions]); const [currentSprintIndex, setCurrentSprintIndex] = useState(0); const currentSprint = processedSprintData[currentSprintIndex]; @@ -397,6 +316,29 @@ export default function Project({projectId}) { const [canShowNextArrow, setCanShowNextArrow] = useState(false); + // <----------------프로젝트 멤버별 기여도 ---------------------> + + useEffect(() => { + const loadContributions = async () => { + if (!currentSprint?.sprint_id) return; + + try { + const contributions = await fetchSprintContributions(currentSprint.sprint_id); + + console.log("기여도 전체 응답:", contributions); + + setSprintContributions(prev => ({ + ...prev, + [currentSprint.sprint_id]: contributions + })); + } catch (error) { + showAlert("error", error.message); + } + }; + + loadContributions(); + }, [currentSprint?.sprint_id]); + const getTaskDataBySprintId = (sprintId) => { const sprint = sprintData.find(s => s.id === sprintId); if (!sprint || !Array.isArray(sprint.taskList)) return []; @@ -613,9 +555,9 @@ export default function Project({projectId}) { } }; - // 멤버 이름 3글자 이상이면 축약 표시 + // 멤버 이름 4글자 이상이면 축약 표시 const reduceMemberName = (name) => { - return name.length > 3 ? name.slice(0, 3) + ".." : name; + return name.length > 4 ? name.slice(0, 4) + ".." : name; }; @@ -789,17 +731,23 @@ currentSprint?.sprint_end === today && isExternalUser === true && ( {reduceMemberName(member.name)} 🥇 - {projectUser && member.id !== projectUser.projectParticipantId && - member.name !== "알수없음" && - - {projectUser.errorClassName !== "PROJECT_PARTICIPATION_REQUIRED" && ( - - )} - - } + {projectUser && + member.id !== projectUser.projectParticipantId && + member.name !== "알수없음" && ( +
+ + {projectUser.errorClassName !== "PROJECT_PARTICIPATION_REQUIRED" && ( + + )} + +
+ )} ))} @@ -809,7 +757,11 @@ currentSprint?.sprint_end === today && isExternalUser === true && ( {isClient && ( m.value > 0) + ? currentSprint.member + : [{ name: "기여도 없음", value: 1, color: "#e0e0e0" }] + } dataKey="value" cx="50%" cy="50%" @@ -819,8 +771,11 @@ currentSprint?.sprint_end === today && isExternalUser === true && ( endAngle={450} stroke="none" > - {currentSprint?.member.map((entry, index) => ( - + {(currentSprint?.member?.some(m => m.value > 0) + ? currentSprint.member + : [{ color: "#e0e0e0" }] + ).map((entry, index) => ( + ))} diff --git a/src/app/project/project_s.js b/src/app/project/project_s.js index 1b7b175..dc4da15 100644 --- a/src/app/project/project_s.js +++ b/src/app/project/project_s.js @@ -314,7 +314,6 @@ export const MeetingDate = styled.span` `; export const FeedbackButton = styled.button` - margin-left: 10px; padding: 5px 10px; font-size: 12px; font-weight: bold; diff --git a/src/app/project/sprint_calendar.js b/src/app/project/sprint_calendar.js index f5318b7..fd4ec0d 100644 --- a/src/app/project/sprint_calendar.js +++ b/src/app/project/sprint_calendar.js @@ -112,6 +112,11 @@ const SprintCalendar = ({sprintStart, sprintEnd, meetingData = [], onMeetingClic return includeYear ? `${year}.${month}.${day}` : `${month}.${day}`; }; + // 팀 미팅명 4글자 이상이면 축약 표시 + const reduceMeetingName = (name) => { + return name.length > 4 ? name.slice(0, 4) + ".." : name; +}; + return (
@@ -184,7 +189,7 @@ const SprintCalendar = ({sprintStart, sprintEnd, meetingData = [], onMeetingClic }} onClick={() => handleMeetingClick(meet)} > - {meet.title} + {reduceMeetingName(meet.title)}
); })} diff --git a/src/app/project/sprint_modal.js b/src/app/project/sprint_modal.js index bf2b541..47c3dd1 100644 --- a/src/app/project/sprint_modal.js +++ b/src/app/project/sprint_modal.js @@ -23,7 +23,7 @@ const SprintTitle = styled.h2` color: #6A4BBF; font-size: 24px; font-weight: bold; - margin-bottom: 30px; + margin-bottom: 50px; `; const GoalInputWrapper = styled.div` @@ -158,24 +158,36 @@ export default function SprintModal({ e.stopPropagation()}> Sprint {title} +

+ * 표시 항목은 필수 항목입니다. +

+ {/* 중간목표 입력 */} - 중간목표 + 중간목표 *