Skip to content

Commit 9bd0cf1

Browse files
authored
Merge pull request #209 from part3-4team-Taskify/minji
[Feat, Refactor, Style] 게스트 모드 추가 / Loading 상태 관리 전역화 / RandomProfile: 색상 고정 / ToDoModal: 컴포넌트 병합
2 parents 512bb83 + a5c31ea commit 9bd0cf1

36 files changed

+546
-422
lines changed

src/api/card.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export const getDashboardMembers = async ({
9494
};
9595

9696
/** 4. 카드 수정 */
97-
export const EditCard = async (cardId: number, data: EditCardPayload) => {
97+
export const editCard = async (cardId: number, data: EditCardPayload) => {
9898
const response = await axiosInstance.put(apiRoutes.cardDetail(cardId), data);
9999
return response.data;
100100
};

src/components/button/CardButton.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,10 @@ const CardButton: React.FC<CardButtonProps> = ({
7979
<svg width="8" height="8" viewBox="0 0 8 8" fill={color}>
8080
<circle cx="4" cy="4" r="4" />
8181
</svg>
82-
<span className="text-black3 text-[14px] sm:text-[16px] truncate max-w-[120px]">
82+
<span
83+
className="flex-1 text-black3 text-[14px] sm:text-[16px]
84+
truncate min-w-0"
85+
>
8386
{title}
8487
</span>
8588
{showCrown && (
@@ -96,7 +99,7 @@ const CardButton: React.FC<CardButtonProps> = ({
9699

97100
{/* 오른쪽: 수정/삭제 버튼 또는 아이콘 */}
98101
{isEditMode ? (
99-
<div className="flex flex-col gap-2">
102+
<div className="flex flex-col gap-2 ml-3 flex-shrink-0 whitespace-nowrap">
100103
{createdByMe && (
101104
<button
102105
onClick={handleEdit}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useRouter } from "next/router";
2+
import useUserStore from "@/store/useUserStore";
3+
import { postAuthData } from "@/api/auth";
4+
import { getUserInfo } from "@/api/users";
5+
import { toast } from "react-toastify";
6+
7+
const GUEST_CREDENTIALS = {
8+
email: "guest@gmail.com",
9+
password: "qwer1155",
10+
};
11+
12+
export default function GuestModeButton() {
13+
const router = useRouter();
14+
const setUser = useUserStore((state) => state.setUser);
15+
16+
const handleGuestLogin = async () => {
17+
try {
18+
const response = await postAuthData(GUEST_CREDENTIALS);
19+
const token = response.accessToken;
20+
localStorage.setItem("accessToken", token);
21+
22+
const userData = await getUserInfo();
23+
setUser(userData);
24+
router.push("/mydashboard");
25+
toast.success("게스트 모드로 로그인되었습니다.");
26+
} catch (error) {
27+
toast.error("게스트 로그인에 실패했습니다.");
28+
}
29+
};
30+
31+
return (
32+
<button
33+
onClick={handleGuestLogin}
34+
className="flex items-center justify-center
35+
sm:w-[220px] w-[280px] h-[54px]
36+
rounded-lg bg-white cursor-pointer
37+
text-[var(--primary)] font-medium sm:text-[18px] text-[16px]"
38+
>
39+
게스트 모드
40+
</button>
41+
);
42+
}

src/components/card/ChangePassword.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { useState } from "react";
22
import { changePassword } from "@/api/changepassword";
33
import Input from "@/components/input/Input";
44
import { toast } from "react-toastify";
5+
import { useUserPermission } from "@/hooks/useUserPermission";
56

67
export default function ChangePassword() {
78
const [password, setPassword] = useState("");
89
const [newPassword, setNewPassword] = useState("");
910
const [checkNewpassword, setCheckNewPassword] = useState("");
1011
const [isSubmitting, setIsSubmitting] = useState(false);
1112

13+
const isGuest = useUserPermission();
14+
1215
const isPasswordMismatch =
1316
!!checkNewpassword && checkNewpassword !== newPassword;
1417
const isDisabled =
@@ -21,6 +24,11 @@ export default function ChangePassword() {
2124
const handleChangePassword = async () => {
2225
if (isDisabled) return;
2326

27+
if (isGuest) {
28+
toast.error("게스트 계정은 정보를 변경할 수 없습니다.");
29+
return;
30+
}
31+
2432
setIsSubmitting(true);
2533

2634
const result = await changePassword({ password, newPassword });

src/components/card/Profile.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Image from "next/image";
44
import { getUserInfo, updateProfile, uploadProfileImage } from "@/api/users";
55
import Input from "@/components/input/Input";
66
import { toast } from "react-toastify";
7+
import { useUserPermission } from "@/hooks/useUserPermission";
78

89
export const ProfileCard = () => {
910
const { user, updateNickname, updateProfileImage } = useUserStore();
@@ -12,6 +13,8 @@ export const ProfileCard = () => {
1213
const [email, setEmail] = useState("");
1314
const [preview, setPreview] = useState<string | null>(null);
1415

16+
const isGuest = useUserPermission();
17+
1518
const fetchUserData = async () => {
1619
try {
1720
const data = await getUserInfo();
@@ -28,6 +31,11 @@ export const ProfileCard = () => {
2831
) => {
2932
const MAX_IMAGE_SIZE = 3.5 * 1024 * 1024;
3033

34+
if (isGuest) {
35+
toast.error("게스트 계정은 정보를 변경할 수 없습니다.");
36+
return;
37+
}
38+
3139
if (event.target.files && event.target.files[0]) {
3240
const file = event.target.files[0];
3341

@@ -54,6 +62,11 @@ export const ProfileCard = () => {
5462
const handleSave = async () => {
5563
if (!nickname) return;
5664

65+
if (isGuest) {
66+
toast.error("게스트 계정은 정보를 변경할 수 없습니다.");
67+
return;
68+
}
69+
5770
try {
5871
const payload: { nickname: string; profileImageUrl?: string } = {
5972
nickname,

src/components/columnCard/Card.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AssigneeType, CardType } from "@/types/task";
22
import Image from "next/image";
33
import { getTagColor } from "../modalInput/chips/ColorTagChip";
4-
import RandomProfile from "../table/member/RandomProfile";
4+
import RandomProfile from "@/components/common/RandomProfile";
55

66
type CardProps = CardType & {
77
imageUrl?: string | null;
@@ -109,7 +109,11 @@ export default function Card({
109109
className="sm:w-[24px] sm:h-[24px] w-[22px] h-[22px] rounded-full
110110
overflow-hidden flex items-center justify-center"
111111
>
112-
<RandomProfile name={assignee.nickname} />
112+
<RandomProfile
113+
userId={assignee.id}
114+
name={assignee.nickname}
115+
className="sm:w-[24px] sm:h-[24px] w-[22px] h-[22px]"
116+
/>
113117
</div>
114118
)}
115119
</div>

src/components/columnCard/Column.tsx

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { useEffect, useState, useRef } from "react";
33
import Image from "next/image";
44
import { CardType } from "@/types/task";
5-
import TodoModal from "@/components/modalInput/ToDoModal";
5+
import TaskModal from "@/components/modalInput/TaskModal";
66
import { TodoButton, ShortTodoButton } from "@/components/button/TodoButton";
77
import ColumnManageModal from "@/components/columnCard/ColumnManageModal";
88
import ColumnDeleteModal from "@/components/columnCard/ColumnDeleteModal";
@@ -14,12 +14,14 @@ import { CardList } from "./CardList";
1414
import CardDetailModal from "@/components/modalDashboard/CardDetailModal";
1515
import { CardDetailType } from "@/types/cards";
1616
import { toast } from "react-toastify";
17+
import { useDashboardPermission } from "@/hooks/useDashboardPermission";
1718

1819
type ColumnProps = {
1920
columnId: number;
2021
title?: string;
2122
tasks?: CardType[];
2223
dashboardId: number;
24+
createdByMe: boolean;
2325
columnDelete: (columnId: number) => void;
2426
fetchColumnsAndCards: () => void;
2527
};
@@ -29,13 +31,16 @@ export default function Column({
2931
title = "new Task",
3032
tasks = [],
3133
dashboardId,
34+
createdByMe,
3235
columnDelete,
3336
fetchColumnsAndCards,
3437
}: ColumnProps) {
38+
const { canEditColumns } = useDashboardPermission(dashboardId, createdByMe);
39+
3540
const [columnTitle, setColumnTitle] = useState(title);
3641
const [isColumnModalOpen, setIsColumnModalOpen] = useState(false);
3742
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
38-
const [isTodoModalOpen, setIsTodoModalOpen] = useState(false);
43+
const [isTaskModalOpen, setIsTaskModalOpen] = useState(false);
3944
const [isCardDetailModalOpen, setIsCardDetailModalOpen] = useState(false);
4045
const [selectedCard, setSelectedCard] = useState<CardDetailType | null>(null);
4146
const [members, setMembers] = useState<
@@ -71,6 +76,7 @@ export default function Column({
7176
toast.error("칼럼 제목을 입력해 주세요.");
7277
return;
7378
}
79+
setIsColumnModalOpen(false);
7480

7581
try {
7682
const updated = await updateColumn({ columnId, title: newTitle });
@@ -132,7 +138,13 @@ export default function Column({
132138
{/* 오른쪽: 생성 버튼 + 설정 버튼 */}
133139
<div className="flex items-center gap-2">
134140
<div
135-
onClick={() => setIsTodoModalOpen(true)}
141+
onClick={() => {
142+
if (!canEditColumns) {
143+
toast.error("읽기 전용 대시보드입니다.");
144+
return;
145+
}
146+
setIsTaskModalOpen(true);
147+
}}
136148
className="block lg:hidden"
137149
>
138150
<ShortTodoButton />
@@ -144,14 +156,26 @@ export default function Column({
144156
fill
145157
priority
146158
className="object-contain cursor-pointer"
147-
onClick={() => setIsColumnModalOpen(true)}
159+
onClick={() => {
160+
if (!canEditColumns) {
161+
toast.error("읽기 전용 대시보드입니다.");
162+
return;
163+
}
164+
setIsColumnModalOpen(true);
165+
}}
148166
/>
149167
</div>
150168
</div>
151169
</div>
152170
<div className="flex items-center justify-center">
153171
<div
154-
onClick={() => setIsTodoModalOpen(true)}
172+
onClick={() => {
173+
if (!canEditColumns) {
174+
toast.error("읽기 전용 대시보드입니다.");
175+
return;
176+
}
177+
setIsTaskModalOpen(true);
178+
}}
155179
className="mb-2 hidden lg:block"
156180
>
157181
<TodoButton />
@@ -177,16 +201,19 @@ export default function Column({
177201
</div>
178202
</div>
179203

180-
{/* Todo 모달 */}
181-
{isTodoModalOpen && (
182-
<TodoModal
183-
isOpen={isTodoModalOpen}
184-
onClose={() => setIsTodoModalOpen(false)}
185-
teamId={TEAM_ID}
204+
{/* 카드 생성 모달 */}
205+
{isTaskModalOpen && (
206+
<TaskModal
207+
mode="create"
208+
isOpen={isTaskModalOpen}
209+
onClose={() => setIsTaskModalOpen(false)}
186210
dashboardId={dashboardId}
187211
columnId={columnId}
188212
members={members}
189-
onChangeCard={fetchColumnsAndCards}
213+
initialData={{
214+
status: columnTitle,
215+
}}
216+
onSubmit={fetchColumnsAndCards}
190217
/>
191218
)}
192219

@@ -220,6 +247,7 @@ export default function Column({
220247
setSelectedCard(null);
221248
}}
222249
onChangeCard={fetchColumnsAndCards}
250+
createdByMe={createdByMe}
223251
/>
224252
)}
225253
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useEffect } from "react";
2+
import { useRouter } from "next/router";
3+
import useLoadingStore from "@/store/useLoadingStore";
4+
5+
export default function GlobalRouteLoadingWatcher() {
6+
const router = useRouter();
7+
const { startLoading, stopLoading } = useLoadingStore();
8+
9+
useEffect(() => {
10+
router.events.on("routeChangeStart", startLoading);
11+
router.events.on("routeChangeComplete", stopLoading);
12+
router.events.on("routeChangeError", stopLoading);
13+
14+
return () => {
15+
router.events.off("routeChangeStart", startLoading);
16+
router.events.off("routeChangeComplete", stopLoading);
17+
router.events.off("routeChangeError", stopLoading);
18+
};
19+
}, [router]);
20+
21+
return null;
22+
}

src/components/common/LoadingSpinner.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import React from "react";
22

33
const LoadingSpinner = () => {
44
return (
5-
<div className="flex justify-center items-center w-full h-screen bg-white">
5+
<div
6+
className="fixed inset-0 z-60 flex justify-center items-center
7+
bg-white transition-opacity duration-300"
8+
>
69
<div className="w-12 h-12 border-6 border-[var(--primary)] border-solid rounded-full animate-spin border-t-transparent" />
710
</div>
811
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
interface RandomProfileProps {
2+
userId: number;
3+
name: string;
4+
className?: string;
5+
}
6+
7+
// 4개의 고정된 색상 배열
8+
const colors = ["bg-[#C4B1A2]", "bg-[#9DD7ED]", "bg-[#FDD446]", "bg-[#FFC85A]"];
9+
10+
// id 숫자 기반 고정 색상 인덱스 생성
11+
function numberToIndex(id: number, length: number): number {
12+
return id % length;
13+
}
14+
15+
export default function RandomProfile({
16+
userId,
17+
name,
18+
className,
19+
}: RandomProfileProps) {
20+
const index = numberToIndex(userId, colors.length);
21+
const bgColor = colors[index];
22+
23+
return (
24+
<div
25+
className={`flex items-center justify-center
26+
leading-none text-white font-medium
27+
rounded-full ${bgColor} ${className}`}
28+
>
29+
{name[0]}
30+
</div>
31+
);
32+
}

0 commit comments

Comments
 (0)