diff --git a/docs/slide-data-flow-legacy.md b/docs/slide-data-flow-legacy.md index c2bb5b66..269dae3e 100644 --- a/docs/slide-data-flow-legacy.md +++ b/docs/slide-data-flow-legacy.md @@ -178,8 +178,8 @@ classDiagram +string content +string script +OpinionItem[] opinions - +HistoryItem[] history - +EmojiReaction[] emojiReactions + +History[] history + +Reaction[] emojiReactions } class OpinionItem { @@ -192,20 +192,20 @@ classDiagram +number parentId } - class HistoryItem { + class History { +string id +string timestamp +string content } - class EmojiReaction { + class Reaction { +string emoji +number count } Slide "1" --> "*" OpinionItem - Slide "1" --> "*" HistoryItem - Slide "1" --> "*" EmojiReaction + Slide "1" --> "*" History + Slide "1" --> "*" Reaction ``` ## 7. 컴포넌트별 Context 사용 diff --git a/docs/slide-data-flow.md b/docs/slide-data-flow.md index fb98ee18..75429f38 100644 --- a/docs/slide-data-flow.md +++ b/docs/slide-data-flow.md @@ -223,8 +223,8 @@ classDiagram +string content +string script +OpinionItem[] opinions - +HistoryItem[] history - +EmojiReaction[] emojiReactions + +History[] history + +Reaction[] emojiReactions } class OpinionItem { @@ -237,21 +237,21 @@ classDiagram +number parentId } - class HistoryItem { + class History { +string id +string timestamp +string content } - class EmojiReaction { + class Reaction { +string emoji +number count } SlideStore --> Slide Slide --> "*" OpinionItem - Slide --> "*" HistoryItem - Slide --> "*" EmojiReaction + Slide --> "*" History + Slide --> "*" Reaction ``` ## 7. Context vs Zustand 비교 diff --git a/public/thumbnails/p1.pdf b/public/thumbnails/p1.pdf new file mode 100644 index 00000000..5cb25ee7 Binary files /dev/null and b/public/thumbnails/p1.pdf differ diff --git a/public/thumbnails/p1/0.webp b/public/thumbnails/p1/0.webp new file mode 100644 index 00000000..1704534b Binary files /dev/null and b/public/thumbnails/p1/0.webp differ diff --git a/public/thumbnails/p1/1.webp b/public/thumbnails/p1/1.webp new file mode 100644 index 00000000..07f0e91d Binary files /dev/null and b/public/thumbnails/p1/1.webp differ diff --git a/public/thumbnails/p1/10.webp b/public/thumbnails/p1/10.webp new file mode 100644 index 00000000..33ba1466 Binary files /dev/null and b/public/thumbnails/p1/10.webp differ diff --git a/public/thumbnails/p1/11.webp b/public/thumbnails/p1/11.webp new file mode 100644 index 00000000..f47d2faf Binary files /dev/null and b/public/thumbnails/p1/11.webp differ diff --git a/public/thumbnails/p1/12.webp b/public/thumbnails/p1/12.webp new file mode 100644 index 00000000..c5811901 Binary files /dev/null and b/public/thumbnails/p1/12.webp differ diff --git a/public/thumbnails/p1/13.webp b/public/thumbnails/p1/13.webp new file mode 100644 index 00000000..920058bc Binary files /dev/null and b/public/thumbnails/p1/13.webp differ diff --git a/public/thumbnails/p1/14.webp b/public/thumbnails/p1/14.webp new file mode 100644 index 00000000..5ab92949 Binary files /dev/null and b/public/thumbnails/p1/14.webp differ diff --git a/public/thumbnails/p1/15.webp b/public/thumbnails/p1/15.webp new file mode 100644 index 00000000..8e4ae86c Binary files /dev/null and b/public/thumbnails/p1/15.webp differ diff --git a/public/thumbnails/p1/16.webp b/public/thumbnails/p1/16.webp new file mode 100644 index 00000000..20c8e697 Binary files /dev/null and b/public/thumbnails/p1/16.webp differ diff --git a/public/thumbnails/p1/17.webp b/public/thumbnails/p1/17.webp new file mode 100644 index 00000000..4f904563 Binary files /dev/null and b/public/thumbnails/p1/17.webp differ diff --git a/public/thumbnails/p1/18.webp b/public/thumbnails/p1/18.webp new file mode 100644 index 00000000..1a2248d6 Binary files /dev/null and b/public/thumbnails/p1/18.webp differ diff --git a/public/thumbnails/p1/19.webp b/public/thumbnails/p1/19.webp new file mode 100644 index 00000000..aeeed724 Binary files /dev/null and b/public/thumbnails/p1/19.webp differ diff --git a/public/thumbnails/p1/2.webp b/public/thumbnails/p1/2.webp new file mode 100644 index 00000000..78c80e0e Binary files /dev/null and b/public/thumbnails/p1/2.webp differ diff --git a/public/thumbnails/p1/20.webp b/public/thumbnails/p1/20.webp new file mode 100644 index 00000000..240fee35 Binary files /dev/null and b/public/thumbnails/p1/20.webp differ diff --git a/public/thumbnails/p1/21.webp b/public/thumbnails/p1/21.webp new file mode 100644 index 00000000..bfbab329 Binary files /dev/null and b/public/thumbnails/p1/21.webp differ diff --git a/public/thumbnails/p1/22.webp b/public/thumbnails/p1/22.webp new file mode 100644 index 00000000..ab89a0e8 Binary files /dev/null and b/public/thumbnails/p1/22.webp differ diff --git a/public/thumbnails/p1/23.webp b/public/thumbnails/p1/23.webp new file mode 100644 index 00000000..fbb08e14 Binary files /dev/null and b/public/thumbnails/p1/23.webp differ diff --git a/public/thumbnails/p1/24.webp b/public/thumbnails/p1/24.webp new file mode 100644 index 00000000..90645522 Binary files /dev/null and b/public/thumbnails/p1/24.webp differ diff --git a/public/thumbnails/p1/25.webp b/public/thumbnails/p1/25.webp new file mode 100644 index 00000000..97811a0f Binary files /dev/null and b/public/thumbnails/p1/25.webp differ diff --git a/public/thumbnails/p1/26.webp b/public/thumbnails/p1/26.webp new file mode 100644 index 00000000..d866140a Binary files /dev/null and b/public/thumbnails/p1/26.webp differ diff --git a/public/thumbnails/p1/27.webp b/public/thumbnails/p1/27.webp new file mode 100644 index 00000000..7eb45337 Binary files /dev/null and b/public/thumbnails/p1/27.webp differ diff --git a/public/thumbnails/p1/28.webp b/public/thumbnails/p1/28.webp new file mode 100644 index 00000000..a23f1961 Binary files /dev/null and b/public/thumbnails/p1/28.webp differ diff --git a/public/thumbnails/p1/29.webp b/public/thumbnails/p1/29.webp new file mode 100644 index 00000000..2de279f9 Binary files /dev/null and b/public/thumbnails/p1/29.webp differ diff --git a/public/thumbnails/p1/3.webp b/public/thumbnails/p1/3.webp new file mode 100644 index 00000000..448a90a9 Binary files /dev/null and b/public/thumbnails/p1/3.webp differ diff --git a/public/thumbnails/p1/30.webp b/public/thumbnails/p1/30.webp new file mode 100644 index 00000000..4d2ec1f2 Binary files /dev/null and b/public/thumbnails/p1/30.webp differ diff --git a/public/thumbnails/p1/31.webp b/public/thumbnails/p1/31.webp new file mode 100644 index 00000000..f8e799ce Binary files /dev/null and b/public/thumbnails/p1/31.webp differ diff --git a/public/thumbnails/p1/32.webp b/public/thumbnails/p1/32.webp new file mode 100644 index 00000000..459ce647 Binary files /dev/null and b/public/thumbnails/p1/32.webp differ diff --git a/public/thumbnails/p1/33.webp b/public/thumbnails/p1/33.webp new file mode 100644 index 00000000..006ceda7 Binary files /dev/null and b/public/thumbnails/p1/33.webp differ diff --git a/public/thumbnails/p1/34.webp b/public/thumbnails/p1/34.webp new file mode 100644 index 00000000..1a369f5c Binary files /dev/null and b/public/thumbnails/p1/34.webp differ diff --git a/public/thumbnails/p1/35.webp b/public/thumbnails/p1/35.webp new file mode 100644 index 00000000..e105d61f Binary files /dev/null and b/public/thumbnails/p1/35.webp differ diff --git a/public/thumbnails/p1/36.webp b/public/thumbnails/p1/36.webp new file mode 100644 index 00000000..cbbde9a4 Binary files /dev/null and b/public/thumbnails/p1/36.webp differ diff --git a/public/thumbnails/p1/37.webp b/public/thumbnails/p1/37.webp new file mode 100644 index 00000000..2d254453 Binary files /dev/null and b/public/thumbnails/p1/37.webp differ diff --git a/public/thumbnails/p1/38.webp b/public/thumbnails/p1/38.webp new file mode 100644 index 00000000..d5504246 Binary files /dev/null and b/public/thumbnails/p1/38.webp differ diff --git a/public/thumbnails/p1/39.webp b/public/thumbnails/p1/39.webp new file mode 100644 index 00000000..3d1886a6 Binary files /dev/null and b/public/thumbnails/p1/39.webp differ diff --git a/public/thumbnails/p1/4.webp b/public/thumbnails/p1/4.webp new file mode 100644 index 00000000..fe73c112 Binary files /dev/null and b/public/thumbnails/p1/4.webp differ diff --git a/public/thumbnails/p1/40.webp b/public/thumbnails/p1/40.webp new file mode 100644 index 00000000..29d22889 Binary files /dev/null and b/public/thumbnails/p1/40.webp differ diff --git a/public/thumbnails/p1/41.webp b/public/thumbnails/p1/41.webp new file mode 100644 index 00000000..b8049adf Binary files /dev/null and b/public/thumbnails/p1/41.webp differ diff --git a/public/thumbnails/p1/42.webp b/public/thumbnails/p1/42.webp new file mode 100644 index 00000000..4a0c58b2 Binary files /dev/null and b/public/thumbnails/p1/42.webp differ diff --git a/public/thumbnails/p1/43.webp b/public/thumbnails/p1/43.webp new file mode 100644 index 00000000..34188e5d Binary files /dev/null and b/public/thumbnails/p1/43.webp differ diff --git a/public/thumbnails/p1/44.webp b/public/thumbnails/p1/44.webp new file mode 100644 index 00000000..bbdc0b6f Binary files /dev/null and b/public/thumbnails/p1/44.webp differ diff --git a/public/thumbnails/p1/45.webp b/public/thumbnails/p1/45.webp new file mode 100644 index 00000000..40026a37 Binary files /dev/null and b/public/thumbnails/p1/45.webp differ diff --git a/public/thumbnails/p1/46.webp b/public/thumbnails/p1/46.webp new file mode 100644 index 00000000..33485131 Binary files /dev/null and b/public/thumbnails/p1/46.webp differ diff --git a/public/thumbnails/p1/47.webp b/public/thumbnails/p1/47.webp new file mode 100644 index 00000000..3853517c Binary files /dev/null and b/public/thumbnails/p1/47.webp differ diff --git a/public/thumbnails/p1/48.webp b/public/thumbnails/p1/48.webp new file mode 100644 index 00000000..8a882ae2 Binary files /dev/null and b/public/thumbnails/p1/48.webp differ diff --git a/public/thumbnails/p1/49.webp b/public/thumbnails/p1/49.webp new file mode 100644 index 00000000..04e0e505 Binary files /dev/null and b/public/thumbnails/p1/49.webp differ diff --git a/public/thumbnails/p1/5.webp b/public/thumbnails/p1/5.webp new file mode 100644 index 00000000..86116c25 Binary files /dev/null and b/public/thumbnails/p1/5.webp differ diff --git a/public/thumbnails/p1/50.webp b/public/thumbnails/p1/50.webp new file mode 100644 index 00000000..26b7ed2b Binary files /dev/null and b/public/thumbnails/p1/50.webp differ diff --git a/public/thumbnails/p1/51.webp b/public/thumbnails/p1/51.webp new file mode 100644 index 00000000..67777ad0 Binary files /dev/null and b/public/thumbnails/p1/51.webp differ diff --git a/public/thumbnails/p1/52.webp b/public/thumbnails/p1/52.webp new file mode 100644 index 00000000..d8d6ff06 Binary files /dev/null and b/public/thumbnails/p1/52.webp differ diff --git a/public/thumbnails/p1/53.webp b/public/thumbnails/p1/53.webp new file mode 100644 index 00000000..3b6914ab Binary files /dev/null and b/public/thumbnails/p1/53.webp differ diff --git a/public/thumbnails/p1/54.webp b/public/thumbnails/p1/54.webp new file mode 100644 index 00000000..95804fa8 Binary files /dev/null and b/public/thumbnails/p1/54.webp differ diff --git a/public/thumbnails/p1/55.webp b/public/thumbnails/p1/55.webp new file mode 100644 index 00000000..aa3e746b Binary files /dev/null and b/public/thumbnails/p1/55.webp differ diff --git a/public/thumbnails/p1/56.webp b/public/thumbnails/p1/56.webp new file mode 100644 index 00000000..7f53e659 Binary files /dev/null and b/public/thumbnails/p1/56.webp differ diff --git a/public/thumbnails/p1/57.webp b/public/thumbnails/p1/57.webp new file mode 100644 index 00000000..7337aab9 Binary files /dev/null and b/public/thumbnails/p1/57.webp differ diff --git a/public/thumbnails/p1/58.webp b/public/thumbnails/p1/58.webp new file mode 100644 index 00000000..e7bfa4e1 Binary files /dev/null and b/public/thumbnails/p1/58.webp differ diff --git a/public/thumbnails/p1/59.webp b/public/thumbnails/p1/59.webp new file mode 100644 index 00000000..3883b8db Binary files /dev/null and b/public/thumbnails/p1/59.webp differ diff --git a/public/thumbnails/p1/6.webp b/public/thumbnails/p1/6.webp new file mode 100644 index 00000000..62428c28 Binary files /dev/null and b/public/thumbnails/p1/6.webp differ diff --git a/public/thumbnails/p1/60.webp b/public/thumbnails/p1/60.webp new file mode 100644 index 00000000..221c9053 Binary files /dev/null and b/public/thumbnails/p1/60.webp differ diff --git a/public/thumbnails/p1/61.webp b/public/thumbnails/p1/61.webp new file mode 100644 index 00000000..426fc95c Binary files /dev/null and b/public/thumbnails/p1/61.webp differ diff --git a/public/thumbnails/p1/62.webp b/public/thumbnails/p1/62.webp new file mode 100644 index 00000000..22f5e896 Binary files /dev/null and b/public/thumbnails/p1/62.webp differ diff --git a/public/thumbnails/p1/63.webp b/public/thumbnails/p1/63.webp new file mode 100644 index 00000000..a95524e5 Binary files /dev/null and b/public/thumbnails/p1/63.webp differ diff --git a/public/thumbnails/p1/64.webp b/public/thumbnails/p1/64.webp new file mode 100644 index 00000000..c85b70a0 Binary files /dev/null and b/public/thumbnails/p1/64.webp differ diff --git a/public/thumbnails/p1/65.webp b/public/thumbnails/p1/65.webp new file mode 100644 index 00000000..3a9fdc55 Binary files /dev/null and b/public/thumbnails/p1/65.webp differ diff --git a/public/thumbnails/p1/66.webp b/public/thumbnails/p1/66.webp new file mode 100644 index 00000000..037e6cd2 Binary files /dev/null and b/public/thumbnails/p1/66.webp differ diff --git a/public/thumbnails/p1/67.webp b/public/thumbnails/p1/67.webp new file mode 100644 index 00000000..7471a730 Binary files /dev/null and b/public/thumbnails/p1/67.webp differ diff --git a/public/thumbnails/p1/68.webp b/public/thumbnails/p1/68.webp new file mode 100644 index 00000000..ff0cf6f2 Binary files /dev/null and b/public/thumbnails/p1/68.webp differ diff --git a/public/thumbnails/p1/69.webp b/public/thumbnails/p1/69.webp new file mode 100644 index 00000000..99f5e69a Binary files /dev/null and b/public/thumbnails/p1/69.webp differ diff --git a/public/thumbnails/p1/7.webp b/public/thumbnails/p1/7.webp new file mode 100644 index 00000000..f7ab72cc Binary files /dev/null and b/public/thumbnails/p1/7.webp differ diff --git a/public/thumbnails/p1/8.webp b/public/thumbnails/p1/8.webp new file mode 100644 index 00000000..761bf47a Binary files /dev/null and b/public/thumbnails/p1/8.webp differ diff --git a/public/thumbnails/p1/9.webp b/public/thumbnails/p1/9.webp new file mode 100644 index 00000000..a4efbff7 Binary files /dev/null and b/public/thumbnails/p1/9.webp differ diff --git a/public/thumbnails/p2.pdf b/public/thumbnails/p2.pdf new file mode 100644 index 00000000..5669a065 Binary files /dev/null and b/public/thumbnails/p2.pdf differ diff --git a/public/thumbnails/p2/0.webp b/public/thumbnails/p2/0.webp new file mode 100644 index 00000000..ba58412d Binary files /dev/null and b/public/thumbnails/p2/0.webp differ diff --git a/public/thumbnails/p2/1.webp b/public/thumbnails/p2/1.webp new file mode 100644 index 00000000..7c8ff26d Binary files /dev/null and b/public/thumbnails/p2/1.webp differ diff --git a/public/thumbnails/p2/10.webp b/public/thumbnails/p2/10.webp new file mode 100644 index 00000000..eccd3aea Binary files /dev/null and b/public/thumbnails/p2/10.webp differ diff --git a/public/thumbnails/p2/11.webp b/public/thumbnails/p2/11.webp new file mode 100644 index 00000000..d0cbc58e Binary files /dev/null and b/public/thumbnails/p2/11.webp differ diff --git a/public/thumbnails/p2/12.webp b/public/thumbnails/p2/12.webp new file mode 100644 index 00000000..80344362 Binary files /dev/null and b/public/thumbnails/p2/12.webp differ diff --git a/public/thumbnails/p2/13.webp b/public/thumbnails/p2/13.webp new file mode 100644 index 00000000..6301b779 Binary files /dev/null and b/public/thumbnails/p2/13.webp differ diff --git a/public/thumbnails/p2/14.webp b/public/thumbnails/p2/14.webp new file mode 100644 index 00000000..0f9ce604 Binary files /dev/null and b/public/thumbnails/p2/14.webp differ diff --git a/public/thumbnails/p2/15.webp b/public/thumbnails/p2/15.webp new file mode 100644 index 00000000..30265e3b Binary files /dev/null and b/public/thumbnails/p2/15.webp differ diff --git a/public/thumbnails/p2/16.webp b/public/thumbnails/p2/16.webp new file mode 100644 index 00000000..804c806a Binary files /dev/null and b/public/thumbnails/p2/16.webp differ diff --git a/public/thumbnails/p2/17.webp b/public/thumbnails/p2/17.webp new file mode 100644 index 00000000..07e3d8a3 Binary files /dev/null and b/public/thumbnails/p2/17.webp differ diff --git a/public/thumbnails/p2/18.webp b/public/thumbnails/p2/18.webp new file mode 100644 index 00000000..d136a634 Binary files /dev/null and b/public/thumbnails/p2/18.webp differ diff --git a/public/thumbnails/p2/19.webp b/public/thumbnails/p2/19.webp new file mode 100644 index 00000000..55eb86ac Binary files /dev/null and b/public/thumbnails/p2/19.webp differ diff --git a/public/thumbnails/p2/2.webp b/public/thumbnails/p2/2.webp new file mode 100644 index 00000000..98bdb0ef Binary files /dev/null and b/public/thumbnails/p2/2.webp differ diff --git a/public/thumbnails/p2/20.webp b/public/thumbnails/p2/20.webp new file mode 100644 index 00000000..2baa84c6 Binary files /dev/null and b/public/thumbnails/p2/20.webp differ diff --git a/public/thumbnails/p2/21.webp b/public/thumbnails/p2/21.webp new file mode 100644 index 00000000..34267548 Binary files /dev/null and b/public/thumbnails/p2/21.webp differ diff --git a/public/thumbnails/p2/22.webp b/public/thumbnails/p2/22.webp new file mode 100644 index 00000000..17f8a72e Binary files /dev/null and b/public/thumbnails/p2/22.webp differ diff --git a/public/thumbnails/p2/23.webp b/public/thumbnails/p2/23.webp new file mode 100644 index 00000000..7bf2ba26 Binary files /dev/null and b/public/thumbnails/p2/23.webp differ diff --git a/public/thumbnails/p2/24.webp b/public/thumbnails/p2/24.webp new file mode 100644 index 00000000..7556b24e Binary files /dev/null and b/public/thumbnails/p2/24.webp differ diff --git a/public/thumbnails/p2/25.webp b/public/thumbnails/p2/25.webp new file mode 100644 index 00000000..f6c343bc Binary files /dev/null and b/public/thumbnails/p2/25.webp differ diff --git a/public/thumbnails/p2/26.webp b/public/thumbnails/p2/26.webp new file mode 100644 index 00000000..d5bfb712 Binary files /dev/null and b/public/thumbnails/p2/26.webp differ diff --git a/public/thumbnails/p2/27.webp b/public/thumbnails/p2/27.webp new file mode 100644 index 00000000..3ed96d16 Binary files /dev/null and b/public/thumbnails/p2/27.webp differ diff --git a/public/thumbnails/p2/28.webp b/public/thumbnails/p2/28.webp new file mode 100644 index 00000000..9a420a3f Binary files /dev/null and b/public/thumbnails/p2/28.webp differ diff --git a/public/thumbnails/p2/29.webp b/public/thumbnails/p2/29.webp new file mode 100644 index 00000000..ca34a3ee Binary files /dev/null and b/public/thumbnails/p2/29.webp differ diff --git a/public/thumbnails/p2/3.webp b/public/thumbnails/p2/3.webp new file mode 100644 index 00000000..aa11283f Binary files /dev/null and b/public/thumbnails/p2/3.webp differ diff --git a/public/thumbnails/p2/30.webp b/public/thumbnails/p2/30.webp new file mode 100644 index 00000000..961bf7a3 Binary files /dev/null and b/public/thumbnails/p2/30.webp differ diff --git a/public/thumbnails/p2/31.webp b/public/thumbnails/p2/31.webp new file mode 100644 index 00000000..239829d0 Binary files /dev/null and b/public/thumbnails/p2/31.webp differ diff --git a/public/thumbnails/p2/32.webp b/public/thumbnails/p2/32.webp new file mode 100644 index 00000000..e69f90f7 Binary files /dev/null and b/public/thumbnails/p2/32.webp differ diff --git a/public/thumbnails/p2/33.webp b/public/thumbnails/p2/33.webp new file mode 100644 index 00000000..d9a2a3bf Binary files /dev/null and b/public/thumbnails/p2/33.webp differ diff --git a/public/thumbnails/p2/34.webp b/public/thumbnails/p2/34.webp new file mode 100644 index 00000000..f52bf13b Binary files /dev/null and b/public/thumbnails/p2/34.webp differ diff --git a/public/thumbnails/p2/35.webp b/public/thumbnails/p2/35.webp new file mode 100644 index 00000000..dc6b35c5 Binary files /dev/null and b/public/thumbnails/p2/35.webp differ diff --git a/public/thumbnails/p2/36.webp b/public/thumbnails/p2/36.webp new file mode 100644 index 00000000..02b0658f Binary files /dev/null and b/public/thumbnails/p2/36.webp differ diff --git a/public/thumbnails/p2/37.webp b/public/thumbnails/p2/37.webp new file mode 100644 index 00000000..2e2852e4 Binary files /dev/null and b/public/thumbnails/p2/37.webp differ diff --git a/public/thumbnails/p2/38.webp b/public/thumbnails/p2/38.webp new file mode 100644 index 00000000..d2fba7e8 Binary files /dev/null and b/public/thumbnails/p2/38.webp differ diff --git a/public/thumbnails/p2/39.webp b/public/thumbnails/p2/39.webp new file mode 100644 index 00000000..ef842c08 Binary files /dev/null and b/public/thumbnails/p2/39.webp differ diff --git a/public/thumbnails/p2/4.webp b/public/thumbnails/p2/4.webp new file mode 100644 index 00000000..9bb6f502 Binary files /dev/null and b/public/thumbnails/p2/4.webp differ diff --git a/public/thumbnails/p2/40.webp b/public/thumbnails/p2/40.webp new file mode 100644 index 00000000..cd902140 Binary files /dev/null and b/public/thumbnails/p2/40.webp differ diff --git a/public/thumbnails/p2/41.webp b/public/thumbnails/p2/41.webp new file mode 100644 index 00000000..c9351160 Binary files /dev/null and b/public/thumbnails/p2/41.webp differ diff --git a/public/thumbnails/p2/42.webp b/public/thumbnails/p2/42.webp new file mode 100644 index 00000000..d3b235c4 Binary files /dev/null and b/public/thumbnails/p2/42.webp differ diff --git a/public/thumbnails/p2/43.webp b/public/thumbnails/p2/43.webp new file mode 100644 index 00000000..6641dff7 Binary files /dev/null and b/public/thumbnails/p2/43.webp differ diff --git a/public/thumbnails/p2/44.webp b/public/thumbnails/p2/44.webp new file mode 100644 index 00000000..a555fc44 Binary files /dev/null and b/public/thumbnails/p2/44.webp differ diff --git a/public/thumbnails/p2/45.webp b/public/thumbnails/p2/45.webp new file mode 100644 index 00000000..b749705e Binary files /dev/null and b/public/thumbnails/p2/45.webp differ diff --git a/public/thumbnails/p2/46.webp b/public/thumbnails/p2/46.webp new file mode 100644 index 00000000..feeb33ac Binary files /dev/null and b/public/thumbnails/p2/46.webp differ diff --git a/public/thumbnails/p2/47.webp b/public/thumbnails/p2/47.webp new file mode 100644 index 00000000..197da461 Binary files /dev/null and b/public/thumbnails/p2/47.webp differ diff --git a/public/thumbnails/p2/48.webp b/public/thumbnails/p2/48.webp new file mode 100644 index 00000000..b3cfb2a6 Binary files /dev/null and b/public/thumbnails/p2/48.webp differ diff --git a/public/thumbnails/p2/49.webp b/public/thumbnails/p2/49.webp new file mode 100644 index 00000000..dcedf2e6 Binary files /dev/null and b/public/thumbnails/p2/49.webp differ diff --git a/public/thumbnails/p2/5.webp b/public/thumbnails/p2/5.webp new file mode 100644 index 00000000..f57bbc6a Binary files /dev/null and b/public/thumbnails/p2/5.webp differ diff --git a/public/thumbnails/p2/50.webp b/public/thumbnails/p2/50.webp new file mode 100644 index 00000000..562b5282 Binary files /dev/null and b/public/thumbnails/p2/50.webp differ diff --git a/public/thumbnails/p2/51.webp b/public/thumbnails/p2/51.webp new file mode 100644 index 00000000..d00b3d9b Binary files /dev/null and b/public/thumbnails/p2/51.webp differ diff --git a/public/thumbnails/p2/52.webp b/public/thumbnails/p2/52.webp new file mode 100644 index 00000000..fdf942a9 Binary files /dev/null and b/public/thumbnails/p2/52.webp differ diff --git a/public/thumbnails/p2/53.webp b/public/thumbnails/p2/53.webp new file mode 100644 index 00000000..09e5cfb8 Binary files /dev/null and b/public/thumbnails/p2/53.webp differ diff --git a/public/thumbnails/p2/54.webp b/public/thumbnails/p2/54.webp new file mode 100644 index 00000000..0740031b Binary files /dev/null and b/public/thumbnails/p2/54.webp differ diff --git a/public/thumbnails/p2/55.webp b/public/thumbnails/p2/55.webp new file mode 100644 index 00000000..05ec8032 Binary files /dev/null and b/public/thumbnails/p2/55.webp differ diff --git a/public/thumbnails/p2/56.webp b/public/thumbnails/p2/56.webp new file mode 100644 index 00000000..373b8ff7 Binary files /dev/null and b/public/thumbnails/p2/56.webp differ diff --git a/public/thumbnails/p2/57.webp b/public/thumbnails/p2/57.webp new file mode 100644 index 00000000..4b0d7eda Binary files /dev/null and b/public/thumbnails/p2/57.webp differ diff --git a/public/thumbnails/p2/58.webp b/public/thumbnails/p2/58.webp new file mode 100644 index 00000000..29dca0ab Binary files /dev/null and b/public/thumbnails/p2/58.webp differ diff --git a/public/thumbnails/p2/6.webp b/public/thumbnails/p2/6.webp new file mode 100644 index 00000000..1b032b13 Binary files /dev/null and b/public/thumbnails/p2/6.webp differ diff --git a/public/thumbnails/p2/7.webp b/public/thumbnails/p2/7.webp new file mode 100644 index 00000000..e2358912 Binary files /dev/null and b/public/thumbnails/p2/7.webp differ diff --git a/public/thumbnails/p2/8.webp b/public/thumbnails/p2/8.webp new file mode 100644 index 00000000..73c86aa3 Binary files /dev/null and b/public/thumbnails/p2/8.webp differ diff --git a/public/thumbnails/p2/9.webp b/public/thumbnails/p2/9.webp new file mode 100644 index 00000000..bb0383c7 Binary files /dev/null and b/public/thumbnails/p2/9.webp differ diff --git a/public/thumbnails/p3.pdf b/public/thumbnails/p3.pdf new file mode 100644 index 00000000..cde5831a Binary files /dev/null and b/public/thumbnails/p3.pdf differ diff --git a/public/thumbnails/p3/0.webp b/public/thumbnails/p3/0.webp new file mode 100644 index 00000000..3a0c4d9a Binary files /dev/null and b/public/thumbnails/p3/0.webp differ diff --git a/public/thumbnails/p3/1.webp b/public/thumbnails/p3/1.webp new file mode 100644 index 00000000..5d268c98 Binary files /dev/null and b/public/thumbnails/p3/1.webp differ diff --git a/public/thumbnails/p3/10.webp b/public/thumbnails/p3/10.webp new file mode 100644 index 00000000..fe99c174 Binary files /dev/null and b/public/thumbnails/p3/10.webp differ diff --git a/public/thumbnails/p3/11.webp b/public/thumbnails/p3/11.webp new file mode 100644 index 00000000..29fd73fb Binary files /dev/null and b/public/thumbnails/p3/11.webp differ diff --git a/public/thumbnails/p3/12.webp b/public/thumbnails/p3/12.webp new file mode 100644 index 00000000..946b2750 Binary files /dev/null and b/public/thumbnails/p3/12.webp differ diff --git a/public/thumbnails/p3/13.webp b/public/thumbnails/p3/13.webp new file mode 100644 index 00000000..17854a1d Binary files /dev/null and b/public/thumbnails/p3/13.webp differ diff --git a/public/thumbnails/p3/14.webp b/public/thumbnails/p3/14.webp new file mode 100644 index 00000000..6913965d Binary files /dev/null and b/public/thumbnails/p3/14.webp differ diff --git a/public/thumbnails/p3/15.webp b/public/thumbnails/p3/15.webp new file mode 100644 index 00000000..9c05c1cb Binary files /dev/null and b/public/thumbnails/p3/15.webp differ diff --git a/public/thumbnails/p3/16.webp b/public/thumbnails/p3/16.webp new file mode 100644 index 00000000..386ab2e4 Binary files /dev/null and b/public/thumbnails/p3/16.webp differ diff --git a/public/thumbnails/p3/2.webp b/public/thumbnails/p3/2.webp new file mode 100644 index 00000000..9c5e4905 Binary files /dev/null and b/public/thumbnails/p3/2.webp differ diff --git a/public/thumbnails/p3/3.webp b/public/thumbnails/p3/3.webp new file mode 100644 index 00000000..d11caf2f Binary files /dev/null and b/public/thumbnails/p3/3.webp differ diff --git a/public/thumbnails/p3/4.webp b/public/thumbnails/p3/4.webp new file mode 100644 index 00000000..bb6079bb Binary files /dev/null and b/public/thumbnails/p3/4.webp differ diff --git a/public/thumbnails/p3/5.webp b/public/thumbnails/p3/5.webp new file mode 100644 index 00000000..2e04984a Binary files /dev/null and b/public/thumbnails/p3/5.webp differ diff --git a/public/thumbnails/p3/6.webp b/public/thumbnails/p3/6.webp new file mode 100644 index 00000000..8bf2acbd Binary files /dev/null and b/public/thumbnails/p3/6.webp differ diff --git a/public/thumbnails/p3/7.webp b/public/thumbnails/p3/7.webp new file mode 100644 index 00000000..94db11ff Binary files /dev/null and b/public/thumbnails/p3/7.webp differ diff --git a/public/thumbnails/p3/8.webp b/public/thumbnails/p3/8.webp new file mode 100644 index 00000000..75e8ce03 Binary files /dev/null and b/public/thumbnails/p3/8.webp differ diff --git a/public/thumbnails/p3/9.webp b/public/thumbnails/p3/9.webp new file mode 100644 index 00000000..f96281cb Binary files /dev/null and b/public/thumbnails/p3/9.webp differ diff --git a/public/thumbnails/p4.pdf b/public/thumbnails/p4.pdf new file mode 100644 index 00000000..2f703c97 Binary files /dev/null and b/public/thumbnails/p4.pdf differ diff --git a/public/thumbnails/p4/0.webp b/public/thumbnails/p4/0.webp new file mode 100644 index 00000000..2086e080 Binary files /dev/null and b/public/thumbnails/p4/0.webp differ diff --git a/public/thumbnails/p4/1.webp b/public/thumbnails/p4/1.webp new file mode 100644 index 00000000..c5acd3a4 Binary files /dev/null and b/public/thumbnails/p4/1.webp differ diff --git a/public/thumbnails/p4/10.webp b/public/thumbnails/p4/10.webp new file mode 100644 index 00000000..b5f9eff8 Binary files /dev/null and b/public/thumbnails/p4/10.webp differ diff --git a/public/thumbnails/p4/11.webp b/public/thumbnails/p4/11.webp new file mode 100644 index 00000000..7709d0bd Binary files /dev/null and b/public/thumbnails/p4/11.webp differ diff --git a/public/thumbnails/p4/12.webp b/public/thumbnails/p4/12.webp new file mode 100644 index 00000000..f5b9ad7b Binary files /dev/null and b/public/thumbnails/p4/12.webp differ diff --git a/public/thumbnails/p4/13.webp b/public/thumbnails/p4/13.webp new file mode 100644 index 00000000..9618557e Binary files /dev/null and b/public/thumbnails/p4/13.webp differ diff --git a/public/thumbnails/p4/14.webp b/public/thumbnails/p4/14.webp new file mode 100644 index 00000000..5c6e04c5 Binary files /dev/null and b/public/thumbnails/p4/14.webp differ diff --git a/public/thumbnails/p4/15.webp b/public/thumbnails/p4/15.webp new file mode 100644 index 00000000..6ea8918b Binary files /dev/null and b/public/thumbnails/p4/15.webp differ diff --git a/public/thumbnails/p4/16.webp b/public/thumbnails/p4/16.webp new file mode 100644 index 00000000..672da796 Binary files /dev/null and b/public/thumbnails/p4/16.webp differ diff --git a/public/thumbnails/p4/17.webp b/public/thumbnails/p4/17.webp new file mode 100644 index 00000000..c7d06086 Binary files /dev/null and b/public/thumbnails/p4/17.webp differ diff --git a/public/thumbnails/p4/18.webp b/public/thumbnails/p4/18.webp new file mode 100644 index 00000000..1eb812e7 Binary files /dev/null and b/public/thumbnails/p4/18.webp differ diff --git a/public/thumbnails/p4/19.webp b/public/thumbnails/p4/19.webp new file mode 100644 index 00000000..11f6a4d9 Binary files /dev/null and b/public/thumbnails/p4/19.webp differ diff --git a/public/thumbnails/p4/2.webp b/public/thumbnails/p4/2.webp new file mode 100644 index 00000000..feffb98d Binary files /dev/null and b/public/thumbnails/p4/2.webp differ diff --git a/public/thumbnails/p4/20.webp b/public/thumbnails/p4/20.webp new file mode 100644 index 00000000..1e2cfcd6 Binary files /dev/null and b/public/thumbnails/p4/20.webp differ diff --git a/public/thumbnails/p4/21.webp b/public/thumbnails/p4/21.webp new file mode 100644 index 00000000..a8a50513 Binary files /dev/null and b/public/thumbnails/p4/21.webp differ diff --git a/public/thumbnails/p4/22.webp b/public/thumbnails/p4/22.webp new file mode 100644 index 00000000..b9a85073 Binary files /dev/null and b/public/thumbnails/p4/22.webp differ diff --git a/public/thumbnails/p4/23.webp b/public/thumbnails/p4/23.webp new file mode 100644 index 00000000..74f71aa1 Binary files /dev/null and b/public/thumbnails/p4/23.webp differ diff --git a/public/thumbnails/p4/24.webp b/public/thumbnails/p4/24.webp new file mode 100644 index 00000000..e198f099 Binary files /dev/null and b/public/thumbnails/p4/24.webp differ diff --git a/public/thumbnails/p4/25.webp b/public/thumbnails/p4/25.webp new file mode 100644 index 00000000..9adf6aaa Binary files /dev/null and b/public/thumbnails/p4/25.webp differ diff --git a/public/thumbnails/p4/26.webp b/public/thumbnails/p4/26.webp new file mode 100644 index 00000000..b6bac748 Binary files /dev/null and b/public/thumbnails/p4/26.webp differ diff --git a/public/thumbnails/p4/27.webp b/public/thumbnails/p4/27.webp new file mode 100644 index 00000000..5aaf5e1c Binary files /dev/null and b/public/thumbnails/p4/27.webp differ diff --git a/public/thumbnails/p4/3.webp b/public/thumbnails/p4/3.webp new file mode 100644 index 00000000..0b276c65 Binary files /dev/null and b/public/thumbnails/p4/3.webp differ diff --git a/public/thumbnails/p4/4.webp b/public/thumbnails/p4/4.webp new file mode 100644 index 00000000..cc0ba68d Binary files /dev/null and b/public/thumbnails/p4/4.webp differ diff --git a/public/thumbnails/p4/5.webp b/public/thumbnails/p4/5.webp new file mode 100644 index 00000000..df2b37fb Binary files /dev/null and b/public/thumbnails/p4/5.webp differ diff --git a/public/thumbnails/p4/6.webp b/public/thumbnails/p4/6.webp new file mode 100644 index 00000000..62dd2c5a Binary files /dev/null and b/public/thumbnails/p4/6.webp differ diff --git a/public/thumbnails/p4/7.webp b/public/thumbnails/p4/7.webp new file mode 100644 index 00000000..e675ee63 Binary files /dev/null and b/public/thumbnails/p4/7.webp differ diff --git a/public/thumbnails/p4/8.webp b/public/thumbnails/p4/8.webp new file mode 100644 index 00000000..d27e1670 Binary files /dev/null and b/public/thumbnails/p4/8.webp differ diff --git a/public/thumbnails/p4/9.webp b/public/thumbnails/p4/9.webp new file mode 100644 index 00000000..f63ca76c Binary files /dev/null and b/public/thumbnails/p4/9.webp differ diff --git a/public/thumbnails/slide-0.webp b/public/thumbnails/slide-0.webp deleted file mode 100644 index e097a759..00000000 Binary files a/public/thumbnails/slide-0.webp and /dev/null differ diff --git a/public/thumbnails/slide-1.webp b/public/thumbnails/slide-1.webp deleted file mode 100644 index dff82718..00000000 Binary files a/public/thumbnails/slide-1.webp and /dev/null differ diff --git a/public/thumbnails/slide-2.webp b/public/thumbnails/slide-2.webp deleted file mode 100644 index 0fad2071..00000000 Binary files a/public/thumbnails/slide-2.webp and /dev/null differ diff --git a/public/thumbnails/slide-3.webp b/public/thumbnails/slide-3.webp deleted file mode 100644 index b8eb6288..00000000 Binary files a/public/thumbnails/slide-3.webp and /dev/null differ diff --git a/public/thumbnails/slide-4.webp b/public/thumbnails/slide-4.webp deleted file mode 100644 index 1118f3b0..00000000 Binary files a/public/thumbnails/slide-4.webp and /dev/null differ diff --git a/public/thumbnails/slide-5.webp b/public/thumbnails/slide-5.webp deleted file mode 100644 index e1610233..00000000 Binary files a/public/thumbnails/slide-5.webp and /dev/null differ diff --git a/public/thumbnails/slide-6.webp b/public/thumbnails/slide-6.webp deleted file mode 100644 index 31d25a85..00000000 Binary files a/public/thumbnails/slide-6.webp and /dev/null differ diff --git a/public/thumbnails/slide-7.webp b/public/thumbnails/slide-7.webp deleted file mode 100644 index 405796eb..00000000 Binary files a/public/thumbnails/slide-7.webp and /dev/null differ diff --git a/public/thumbnails/slide-8.webp b/public/thumbnails/slide-8.webp deleted file mode 100644 index 2e1da830..00000000 Binary files a/public/thumbnails/slide-8.webp and /dev/null differ diff --git a/public/thumbnails/slide-9.webp b/public/thumbnails/slide-9.webp deleted file mode 100644 index 1959a02b..00000000 Binary files a/public/thumbnails/slide-9.webp and /dev/null differ diff --git a/src/api/endpoints/opinions.ts b/src/api/endpoints/opinions.ts index fd52e4c6..e9c5c21b 100644 --- a/src/api/endpoints/opinions.ts +++ b/src/api/endpoints/opinions.ts @@ -3,7 +3,7 @@ * @description 의견(댓글) 관련 API 엔드포인트 */ import { apiClient } from '@/api'; -import type { CommentItem } from '@/types/comment'; +import type { Comment } from '@/types/comment'; /** * 의견 생성 요청 타입 @@ -21,11 +21,8 @@ export interface CreateOpinionRequest { * @param data - 의견 데이터 * @returns 생성된 의견 */ -export async function createOpinion( - slideId: string, - data: CreateOpinionRequest, -): Promise { - const response = await apiClient.post(`/slides/${slideId}/opinions`, data); +export async function createOpinion(slideId: string, data: CreateOpinionRequest): Promise { + const response = await apiClient.post(`/slides/${slideId}/opinions`, data); return response.data; } diff --git a/src/api/endpoints/reactions.ts b/src/api/endpoints/reactions.ts index 4a393d83..3bbdfe73 100644 --- a/src/api/endpoints/reactions.ts +++ b/src/api/endpoints/reactions.ts @@ -1,14 +1,11 @@ import { apiClient } from '@/api'; -import type { EmojiReaction, ReactionType } from '@/types/script'; +import type { Reaction, ReactionType } from '@/types/script'; export interface ToggleReactionRequest { type: ReactionType; } export const toggleReaction = async (slideId: string, data: ToggleReactionRequest) => { - const { data: response } = await apiClient.post( - `/slides/${slideId}/reactions`, - data, - ); + const { data: response } = await apiClient.post(`/slides/${slideId}/reactions`, data); return response; }; diff --git a/src/components/comment/CommentItem.tsx b/src/components/comment/Comment.tsx similarity index 96% rename from src/components/comment/CommentItem.tsx rename to src/components/comment/Comment.tsx index fb2a8194..9e2fa452 100644 --- a/src/components/comment/CommentItem.tsx +++ b/src/components/comment/Comment.tsx @@ -1,5 +1,5 @@ /** - * @file CommentItem.tsx + * @file Comment.tsx * @description 댓글 항목 공통 컴포넌트 * * 슬라이드 화면(CommentPopover)과 피드백 화면(CommentList) 모두에서 사용됩니다. @@ -13,14 +13,14 @@ import FileIcon from '@/assets/icons/icon-document.svg?react'; import RemoveIcon from '@/assets/icons/icon-remove.svg?react'; import ReplyIcon from '@/assets/icons/icon-reply.svg?react'; import { MOCK_USERS } from '@/mocks/users'; -import type { CommentItem as CommentItemType } from '@/types/comment'; +import type { Comment as CommentType } from '@/types/comment'; import { formatRelativeTime } from '@/utils/format'; import CommentInput from './CommentInput'; -interface CommentItemProps { +interface CommentProps { /** 댓글 데이터 */ - comment: CommentItemType; + comment: CommentType; /** 답글 입력창 활성화 여부 */ isActive: boolean; /** 답글 입력값 */ @@ -57,7 +57,7 @@ interface CommentItemProps { * 댓글 내용, 작성자 정보, 답글 버튼, 삭제 버튼을 표시합니다. * 대댓글은 재귀적으로 렌더링됩니다. */ -function CommentItem({ +function Comment({ comment, isActive, replyText, @@ -73,7 +73,7 @@ function CommentItem({ setReplyingToId, onReplySubmit, onToggleReplyById, -}: CommentItemProps) { +}: CommentProps) { const user = MOCK_USERS.find((u) => u.id === comment.authorId); const authorName = user?.name ?? '알 수 없음'; const authorProfileImage = user?.profileImage; @@ -199,7 +199,7 @@ function CommentItem({ {comment.replies && comment.replies.length > 0 && (
{comment.replies.map((reply) => ( - void; onGoToSlideRef: (ref: string) => void; onDeleteComment?: (commentId: string) => void; @@ -48,7 +48,7 @@ export default function CommentList({ return (
{comments.map((comment) => ( - {opinions.map((opinion) => ( - diff --git a/src/components/common/FileDropzone.tsx b/src/components/common/FileDropzone.tsx index 13896ee6..b5823668 100644 --- a/src/components/common/FileDropzone.tsx +++ b/src/components/common/FileDropzone.tsx @@ -1,10 +1,18 @@ import { useRef, useState } from 'react'; import UploadIcon from '@/assets/icons/icon-upload.svg?react'; -import type { FileDropProps } from '@/types/uploadFile'; +import type { UploadState } from '@/types/uploadFile'; import ProgressBar from './ProgressBar'; +interface FileDropProps { + onFilesSelected: (files: File[]) => void; + accept?: string; + disabled?: boolean; + uploadState?: UploadState; + progress?: number; +} + export default function FileDropzone({ onFilesSelected, accept, diff --git a/src/components/common/SlideImage.tsx b/src/components/common/SlideImage.tsx index cb23055b..9c17775d 100644 --- a/src/components/common/SlideImage.tsx +++ b/src/components/common/SlideImage.tsx @@ -18,17 +18,15 @@ export default function SlideImage({ src, alt }: SlideImageProps) { const [isLoaded, setIsLoaded] = useState(false); return ( - <> - {!isLoaded &&
} - {alt} setIsLoaded(true)} - className={clsx( - 'h-full w-full object-cover transition-opacity duration-300', - isLoaded ? 'opacity-100' : 'opacity-0', - )} - /> - + {alt} setIsLoaded(true)} + className={clsx( + 'block w-full h-auto transition-opacity duration-300', + !isLoaded && 'animate-pulse bg-gray-200', + isLoaded ? 'opacity-100' : 'opacity-0', + )} + /> ); } diff --git a/src/components/common/index.ts b/src/components/common/index.ts index c532f2ef..2b5c40c6 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -1,4 +1,4 @@ -export { default as CommentItem } from '../comment/CommentItem'; +export { default as Comment } from '../comment/Comment'; export { Gnb } from './layout/Gnb'; export { Layout } from './layout/Layout'; diff --git a/src/components/feedback/ReactionButtons.tsx b/src/components/feedback/ReactionButtons.tsx index 086cc974..8b723b4e 100644 --- a/src/components/feedback/ReactionButtons.tsx +++ b/src/components/feedback/ReactionButtons.tsx @@ -4,11 +4,12 @@ * * 피드백 화면 하단에서 슬라이드에 대한 리액션을 표시합니다. */ -import { type EmojiReaction, REACTION_CONFIG, type ReactionType } from '@/types/script'; +import { REACTION_CONFIG } from '@/constants/reaction'; +import type { Reaction, ReactionType } from '@/types/script'; interface ReactionButtonsProps { /** 리액션 목록 (타입, 카운트, 활성화 여부) */ - reactions: EmojiReaction[]; + reactions: Reaction[]; /** 리액션 토글 핸들러 */ onToggleReaction: (type: ReactionType) => void; } diff --git a/src/components/home/IntroSection.tsx b/src/components/home/IntroSection.tsx index 7f263747..e63e88d6 100644 --- a/src/components/home/IntroSection.tsx +++ b/src/components/home/IntroSection.tsx @@ -1,9 +1,19 @@ import clsx from 'clsx'; -import type { IntroSectionProps } from '@/types/home'; +import type { UploadState } from '@/types/uploadFile'; import FileDropzone from '../common/FileDropzone'; +interface IntroSectionProps { + accept: string; + disabled: boolean; + uploadState: UploadState; + progress: number; + error?: string | null; + onFilesSelected: (files: File[]) => void; + isEmpty: boolean; +} + export default function IntroSection({ accept, disabled, @@ -23,7 +33,7 @@ export default function IntroSection({ {/* 소개글 */}

발표 연습을 시작하세요.

-

+

파일을 업로드해서 바로 연습을 시작해보세요.

diff --git a/src/components/home/ProjectsSection.tsx b/src/components/home/ProjectsSection.tsx index b2e8b99c..c832dfb3 100644 --- a/src/components/home/ProjectsSection.tsx +++ b/src/components/home/ProjectsSection.tsx @@ -1,4 +1,5 @@ -import type { CardItems } from '@/types/project'; +import type { SortMode, ViewMode } from '@/types/home'; +import type { Project } from '@/types/project'; import ProjectCard from '../projects/ProjectCard'; import { ProjectCardSkeleton } from '../projects/ProjectCardSkeleton'; @@ -10,10 +11,21 @@ type Props = { isLoading: boolean; query: string; onChangeQuery: (value: string) => void; - projects: CardItems[]; + onChangeSort: (value: SortMode) => void; + viewMode: ViewMode; + onChangeViewMode: (value: ViewMode) => void; + projects: Project[]; }; -export default function ProjectsSection({ isLoading, query, onChangeQuery, projects }: Props) { +export default function ProjectsSection({ + isLoading, + query, + onChangeQuery, + onChangeSort, + viewMode, + onChangeViewMode, + projects, +}: Props) { const hasProjects = projects.length > 0; if (!isLoading && !hasProjects) return null; @@ -25,8 +37,14 @@ export default function ProjectsSection({ isLoading, query, onChangeQuery, proje

내 발표

- {/* 검색 */} - + {/* 검색 및 필터 */} + {/* 프레젠테이션 목록 */}
diff --git a/src/components/projects/ProjectCard.tsx b/src/components/projects/ProjectCard.tsx index a7b97bb9..4f5e663e 100644 --- a/src/components/projects/ProjectCard.tsx +++ b/src/components/projects/ProjectCard.tsx @@ -1,3 +1,5 @@ +import { useNavigate } from 'react-router-dom'; + import clsx from 'clsx'; import CommentCountIcon from '@/assets/icons/icon-comment-count.svg?react'; @@ -5,11 +7,15 @@ import MoreIcon from '@/assets/icons/icon-more.svg?react'; import PageCountIcon from '@/assets/icons/icon-page-count.svg?react'; import ReactionCountIcon from '@/assets/icons/icon-reaction-count.svg?react'; import ViewCountIcon from '@/assets/icons/icon-view-count.svg?react'; -import type { CardItems } from '@/types/project'; +import { getTabPath } from '@/constants/navigation'; +import type { Project } from '@/types/project'; +import { formatRelativeTime } from '@/utils/format'; -import { Popover } from '../common'; +import { Dropdown } from '../common'; +import type { DropdownItem } from '../common/Dropdown'; export default function ProjectCard({ + id, title, updatedAt, pageCount, @@ -17,13 +23,40 @@ export default function ProjectCard({ reactionCount, viewCount = 0, thumbnailUrl, -}: CardItems) { +}: Project) { + const navigate = useNavigate(); + + const dropdownItems: DropdownItem[] = [ + { + id: 'rename', + label: '이름 변경', + onClick: () => { + // TODO: 이름 변경 로직 구현 + }, + }, + { + id: 'delete', + label: '삭제', + variant: 'danger', + onClick: () => { + // TODO: 삭제 로직 구현 + }, + }, + ]; + + const handleCardClick = () => { + navigate(getTabPath(id, 'slide')); + }; + return ( -
-
+
+
{thumbnailUrl && ( {`${title}`} @@ -32,33 +65,24 @@ export default function ProjectCard({
{/* 제목 및 업데이트 날짜 */} -
+

{title}

- ( - - )} - position="bottom" - align="end" - ariaLabel="더보기" - className="border border-gray-200 w-32 overflow-hidden" - > -
- - -
-
+
e.stopPropagation()}> + ( + + )} + items={dropdownItems} + position="bottom" + align="end" + ariaLabel="더보기" + menuClassName="w-32" + /> +
-

{updatedAt}

+

{formatRelativeTime(updatedAt)}

-
+
{pageCount} 페이지 diff --git a/src/components/projects/ProjectHeader.tsx b/src/components/projects/ProjectHeader.tsx index 3e091fb1..dc33a3eb 100644 --- a/src/components/projects/ProjectHeader.tsx +++ b/src/components/projects/ProjectHeader.tsx @@ -5,28 +5,37 @@ import FilterIcon from '@/assets/icons/icon-filter.svg?react'; import SearchIcon from '@/assets/icons/icon-search.svg?react'; import ViewCardIcon from '@/assets/icons/icon-view-card.svg?react'; import ViewListIcon from '@/assets/icons/icon-view-list.svg?react'; -import type { ProjectHeaderProps } from '@/types/project'; +import type { SortMode, ViewMode } from '@/types/home'; import { Dropdown } from '../common'; -// TODO -// - 필터, 정렬 기능 추가 -// ㄴ> onClick에 상태 업데이트 추가 +interface ProjectHeaderProps { + value: string; + onChange: (value: string) => void; + onChangeSort: (sort: SortMode) => void; + viewMode: ViewMode; + onChangeViewMode: (viewMode: ViewMode) => void; +} -//검색 + 우측 컨트롤 -export default function ProjectHeader({ value, onChange }: ProjectHeaderProps) { +export default function ProjectHeader({ + value, + onChange, + onChangeSort, + viewMode, + onChangeViewMode, +}: ProjectHeaderProps) { return (
{/* 검색 부분 */}
onChange(e.target.value)} /> - +
@@ -37,7 +46,7 @@ export default function ProjectHeader({ value, onChange }: ProjectHeaderProps) { type="button" className={clsx( 'flex items-center gap-2 rounded-lg px-2 py-2 cursor-pointer text-body-m-bold', - isOpen ? 'text-main' : 'text-gray-700', + isOpen ? 'text-main' : 'text-gray-600', )} > 필터 @@ -55,14 +64,14 @@ export default function ProjectHeader({ value, onChange }: ProjectHeaderProps) { ]} /> -
+
( -
diff --git a/src/components/slide/SlideList.tsx b/src/components/slide/SlideList.tsx index 355f43c1..b44b074f 100644 --- a/src/components/slide/SlideList.tsx +++ b/src/components/slide/SlideList.tsx @@ -21,8 +21,6 @@ interface SlideListProps { slides?: Slide[]; /** 현재 선택된 슬라이드 ID */ currentSlideId?: string; - /** 슬라이드 링크 기본 경로 */ - basePath: string; /** 로딩 상태 */ isLoading?: boolean; } @@ -33,7 +31,7 @@ interface SlideListProps { * - 위/아래 화살표 키로 슬라이드 이동 * - 현재 슬라이드 변경 시 자동 스크롤 */ -export default function SlideList({ slides, currentSlideId, basePath, isLoading }: SlideListProps) { +export default function SlideList({ slides, currentSlideId, isLoading }: SlideListProps) { const navigate = useNavigate(); const listRef = useRef(null); @@ -42,9 +40,9 @@ export default function SlideList({ slides, currentSlideId, basePath, isLoading const navigateToSlide = useCallback( (index: number) => { if (!slides || index < 0 || index >= slides.length) return; - navigate(`${basePath}/slide/${slides[index].id}`); + navigate({ search: `?slideId=${slides[index].id}` }, { replace: true }); }, - [slides, basePath, navigate], + [slides, navigate], ); const keyMap = useMemo( @@ -82,7 +80,6 @@ export default function SlideList({ slides, currentSlideId, basePath, isLoading slide={slide} index={idx} isActive={slide.id === currentSlideId} - basePath={basePath} /> ))}
diff --git a/src/components/slide/SlideThumbnail.tsx b/src/components/slide/SlideThumbnail.tsx index 4f8ed983..b75dbe6d 100644 --- a/src/components/slide/SlideThumbnail.tsx +++ b/src/components/slide/SlideThumbnail.tsx @@ -19,8 +19,6 @@ interface SlideThumbnailProps { index: number; /** 현재 선택된 슬라이드 여부 */ isActive?: boolean; - /** 슬라이드 링크 기본 경로 */ - basePath?: string; /** 로딩 상태 (스켈레톤 표시) */ isLoading?: boolean; } @@ -35,7 +33,6 @@ export default function SlideThumbnail({ slide, index, isActive = false, - basePath = '', isLoading, }: SlideThumbnailProps) { if (isLoading || !slide) { @@ -49,7 +46,8 @@ export default function SlideThumbnail({ return ( {/* 썸네일 */} -
+
diff --git a/src/components/slide/SlideViewer.tsx b/src/components/slide/SlideViewer.tsx index ea648da1..28e5e1cb 100644 --- a/src/components/slide/SlideViewer.tsx +++ b/src/components/slide/SlideViewer.tsx @@ -5,7 +5,7 @@ * 슬라이드 이미지를 표시하며, 로딩 상태일 때 스켈레톤을 보여줍니다. * ScriptBox의 접힘 상태에 따라 위치가 조정됩니다. */ -import { Skeleton, SlideImage } from '@/components/common'; +import { SlideImage } from '@/components/common'; import { SLIDE_MAX_WIDTH } from '@/constants/layout'; import { useSlideThumb, useSlideTitle } from '@/hooks'; @@ -21,9 +21,9 @@ export default function SlideViewer({ isLoading }: SlideViewerProps) { return (
-
+
{isLoading ? ( - +
) : ( thumb && )} diff --git a/src/components/slide/script/CommentPopover.tsx b/src/components/slide/script/CommentPopover.tsx index bdbf3ef6..c094ad02 100644 --- a/src/components/slide/script/CommentPopover.tsx +++ b/src/components/slide/script/CommentPopover.tsx @@ -9,7 +9,7 @@ import { useState } from 'react'; import clsx from 'clsx'; -import CommentItem from '@/components/comment/CommentItem'; +import Comment from '@/components/comment/Comment'; import { Popover, Skeleton } from '@/components/common'; import { useSlideOpinions } from '@/hooks'; import { useComments } from '@/hooks/useComments'; @@ -95,7 +95,7 @@ export default function CommentPopover({ isLoading }: CommentPopoverProps) { {/* 의견 목록 */}
{treeOpinions.map((opinion) => ( - { + const handleRestore = (item: History) => { if (!slideId) return; // 1. 로컬 상태 업데이트 (Optimistic) diff --git a/src/constants/navigation.ts b/src/constants/navigation.ts index 20d48106..59693674 100644 --- a/src/constants/navigation.ts +++ b/src/constants/navigation.ts @@ -49,7 +49,7 @@ export const getLastSlideId = (projectId: string): string => { export const getTabPath = (projectId: string, tab: Tab, slideId?: string): string => { switch (tab) { case 'slide': - return `/${projectId}/slide/${slideId ?? getLastSlideId(projectId)}`; + return `/${projectId}/slide?slideId=${slideId ?? getLastSlideId(projectId)}`; case 'video': return `/${projectId}/video`; case 'insight': diff --git a/src/constants/reaction.ts b/src/constants/reaction.ts new file mode 100644 index 00000000..2fcbbd12 --- /dev/null +++ b/src/constants/reaction.ts @@ -0,0 +1,12 @@ +import type { ReactionType } from '@/types/script'; + +/** + * 리액션 설정 (이모지, 라벨 매핑) + */ +export const REACTION_CONFIG: Record = { + fire: { emoji: '🔥', label: '인상적이에요' }, + sleepy: { emoji: '💤', label: '지루해요' }, + good: { emoji: '👍', label: '잘했어요' }, + bad: { emoji: '👎', label: '별로에요' }, + confused: { emoji: '🤷', label: '이해 안돼요' }, +} as const; diff --git a/src/hooks/queries/useOpinions.ts b/src/hooks/queries/useOpinions.ts index 7af1e495..f6c14dec 100644 --- a/src/hooks/queries/useOpinions.ts +++ b/src/hooks/queries/useOpinions.ts @@ -1,25 +1,12 @@ /** - * @file useOpinions.ts - * @description 의견 관련 TanStack Query 훅 + * 의견 관련 TanStack Query 훅 */ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { type CreateOpinionRequest, createOpinion, deleteOpinion } from '@/api/endpoints/opinions'; import { queryKeys } from '@/api/queryClient'; -/** - * 의견 추가 훅 - * - * @example - * const { mutate: addOpinion, isPending } = useCreateOpinion(); - * - * const handleSubmit = () => { - * addOpinion( - * { slideId: '1', data: { content: '좋은 의견이에요!' } }, - * { onSuccess: () => console.log('추가 완료') } - * ); - * }; - */ +/** 의견 추가 */ export function useCreateOpinion() { const queryClient = useQueryClient(); @@ -28,25 +15,12 @@ export function useCreateOpinion() { createOpinion(slideId, data), onSuccess: (_, { slideId }) => { - // 해당 슬라이드 캐시 무효화 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); }, }); } -/** - * 의견 삭제 훅 - * - * @example - * const { mutate: removeOpinion, isPending } = useDeleteOpinion(); - * - * const handleDelete = () => { - * removeOpinion( - * { opinionId: 'op-1', slideId: '1' }, - * { onSuccess: () => console.log('삭제 완료') } - * ); - * }; - */ +/** 의견 삭제 */ export function useDeleteOpinion() { const queryClient = useQueryClient(); @@ -54,7 +28,6 @@ export function useDeleteOpinion() { mutationFn: ({ opinionId }: { opinionId: string; slideId: string }) => deleteOpinion(opinionId), onSuccess: (_, { slideId }) => { - // 해당 슬라이드 캐시 무효화 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); }, }); diff --git a/src/hooks/queries/useReactionQueries.ts b/src/hooks/queries/useReactionQueries.ts index 0578f5c7..ef8b3a7b 100644 --- a/src/hooks/queries/useReactionQueries.ts +++ b/src/hooks/queries/useReactionQueries.ts @@ -1,12 +1,12 @@ /** - * @file useReactionQueries.ts - * @description 리액션(이모지) 관련 TanStack Query 훅 + * 리액션 관련 TanStack Query 훅 */ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { type ToggleReactionRequest, toggleReaction } from '@/api/endpoints/reactions'; import { queryKeys } from '@/api/queryClient'; +/** 리액션 토글 */ export function useToggleReaction() { const queryClient = useQueryClient(); @@ -15,7 +15,6 @@ export function useToggleReaction() { toggleReaction(slideId, data), onSuccess: (_, { slideId }) => { - // 해당 슬라이드 캐시 무효화 -> 최신 리액션 정보 반영 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); }, }); diff --git a/src/hooks/queries/useSlides.ts b/src/hooks/queries/useSlides.ts index 0630c71f..7af96c88 100644 --- a/src/hooks/queries/useSlides.ts +++ b/src/hooks/queries/useSlides.ts @@ -1,12 +1,5 @@ /** - * @file useSlides.ts - * @description 슬라이드 관련 TanStack Query 훅 - * - * 이 훅들을 컴포넌트에서 사용하면: - * - 로딩 상태 자동 관리 (isLoading) - * - 에러 상태 자동 관리 (isError, error) - * - 캐싱 자동 처리 - * - 백그라운드 리패치 + * 슬라이드 관련 TanStack Query 훅 */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; @@ -20,39 +13,16 @@ import { } from '@/api/endpoints/slides'; import { queryKeys } from '@/api/queryClient'; -/** - * 슬라이드 목록 조회 훅 - * - * @param projectId - 프로젝트 ID - * - * @example - * function SlideList({ projectId }) { - * const { data: slides, isLoading, isError } = useSlides(projectId); - * - * if (isLoading) return
로딩 중...
; - * if (isError) return
에러 발생!
; - * - * return slides.map(slide =>
{slide.title}
); - * } - */ +/** 슬라이드 목록 조회 */ export function useSlides(projectId: string) { return useQuery({ - // 캐시 키 - 이 키로 캐시를 식별하고 무효화함 queryKey: queryKeys.slides.list(projectId), - - // 실제 데이터 fetching 함수 queryFn: () => getSlides(projectId), - - // projectId가 있을 때만 요청 enabled: !!projectId, }); } -/** - * 단일 슬라이드 조회 훅 - * - * @param slideId - 슬라이드 ID - */ +/** 단일 슬라이드 조회 */ export function useSlide(slideId: string) { return useQuery({ queryKey: queryKeys.slides.detail(slideId), @@ -61,47 +31,22 @@ export function useSlide(slideId: string) { }); } -/** - * 슬라이드 수정 훅 (mutation) - * - * @example - * function EditSlide({ slideId }) { - * const { mutate: update, isPending } = useUpdateSlide(); - * - * const handleSave = () => { - * update( - * { slideId, data: { title: '새 제목' } }, - * { - * onSuccess: () => alert('저장 완료!'), - * onError: () => alert('저장 실패!'), - * } - * ); - * }; - * - * return ; - * } - */ +/** 슬라이드 수정 */ export function useUpdateSlide() { const queryClient = useQueryClient(); return useMutation({ - // mutation 함수 mutationFn: ({ slideId, data }: { slideId: string; data: UpdateSlideRequest }) => updateSlide(slideId, data), - // 성공 시 관련 쿼리 캐시 무효화 (자동 리패치) onSuccess: (_, { slideId }) => { - // 해당 슬라이드 상세 캐시 무효화 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.detail(slideId) }); - // 슬라이드 목록 캐시도 무효화 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.lists() }); }, }); } -/** - * 슬라이드 생성 훅 - */ +/** 슬라이드 생성 */ export function useCreateSlide() { const queryClient = useQueryClient(); @@ -115,15 +60,12 @@ export function useCreateSlide() { }) => createSlide(projectId, data), onSuccess: (_, { projectId }) => { - // 슬라이드 목록 캐시 무효화 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.list(projectId) }); }, }); } -/** - * 슬라이드 삭제 훅 - */ +/** 슬라이드 삭제 */ export function useDeleteSlide() { const queryClient = useQueryClient(); @@ -131,7 +73,6 @@ export function useDeleteSlide() { mutationFn: (slideId: string) => deleteSlide(slideId), onSuccess: () => { - // 모든 슬라이드 관련 캐시 무효화 void queryClient.invalidateQueries({ queryKey: queryKeys.slides.all }); }, }); diff --git a/src/hooks/useComments.ts b/src/hooks/useComments.ts index 22b34f82..bc887882 100644 --- a/src/hooks/useComments.ts +++ b/src/hooks/useComments.ts @@ -1,20 +1,23 @@ /** - * @file useComments.ts - * @description 댓글/의견 관련 통합 훅 + * 댓글/의견 통합 훅 * - * SlidePage(Opinion)와 FeedbackSlidePage(CommentList) 모두에서 사용됩니다. - * Optimistic UI 패턴으로 로컬 store 업데이트 후 API 호출합니다. + * Optimistic UI 패턴으로 로컬 store 업데이트 후 API를 호출합니다. + * + * @returns comments - 트리 구조의 댓글 목록 + * @returns addComment - 새 댓글 추가 + * @returns addReply - 답글 추가 + * @returns deleteComment - 댓글 삭제 */ import { useMemo } from 'react'; import { useSlideStore } from '@/stores/slideStore'; -import type { CommentItem } from '@/types/comment'; +import type { Comment } from '@/types/comment'; import { flatToTree } from '@/utils/comment'; import { showToast } from '@/utils/toast'; import { useCreateOpinion, useDeleteOpinion } from './queries/useOpinions'; -const EMPTY_COMMENTS: CommentItem[] = []; +const EMPTY_COMMENTS: Comment[] = []; export function useComments() { const slideId = useSlideStore((state) => state.slide?.id); @@ -27,15 +30,11 @@ export function useComments() { const { mutate: createOpinionApi } = useCreateOpinion(); const { mutate: deleteOpinionApi } = useDeleteOpinion(); - // FeedbackSlidePage의 CommentList에서 사용 (tree 구조) const comments = useMemo(() => { if (!flatComments) return EMPTY_COMMENTS; return flatToTree(flatComments); }, [flatComments]); - /** - * 새 댓글 추가 (Optimistic UI) - */ const addComment = (content: string, currentSlideIndex: number) => { if (!slideId) return; @@ -53,9 +52,6 @@ export function useComments() { ); }; - /** - * 답글 추가 (Optimistic UI) - */ const addReply = (parentId: string, content: string) => { if (!slideId) return; @@ -73,9 +69,6 @@ export function useComments() { ); }; - /** - * 댓글 삭제 (Optimistic UI) - */ const deleteComment = (commentId: string) => { if (!slideId) return; diff --git a/src/hooks/useHomeSelectors.ts b/src/hooks/useHomeSelectors.ts index bc224132..483a735e 100644 --- a/src/hooks/useHomeSelectors.ts +++ b/src/hooks/useHomeSelectors.ts @@ -1,11 +1,22 @@ +/** + * 홈 스토어 셀렉터 훅 + * + * 필요한 상태만 구독하여 불필요한 리렌더링을 방지합니다. + */ import { useShallow } from 'zustand/shallow'; import { useHomeStore } from '@/stores/homeStore'; +/** 검색어 구독 */ export const useHomeQuery = () => useHomeStore((s) => s.query); + +/** 보기 모드 구독 ('card' | 'list') */ export const useHomeViewMode = () => useHomeStore((s) => s.viewMode); + +/** 정렬 모드 구독 ('recent' | 'commentCount' | 'name') */ export const useHomeSort = () => useHomeStore((s) => s.sort); +/** 홈 스토어 액션들 (참조 안정적) */ export const useHomeActions = () => useHomeStore( useShallow((s) => ({ diff --git a/src/hooks/useMediaStream.ts b/src/hooks/useMediaStream.ts index d33b252b..ee87a60a 100644 --- a/src/hooks/useMediaStream.ts +++ b/src/hooks/useMediaStream.ts @@ -1,3 +1,16 @@ +/** + * 미디어 스트림 훅 + * + * 카메라/마이크 스트림과 오디오 볼륨 분석을 제공합니다. + * + * @param videoDeviceId - 비디오 장치 ID + * @param audioDeviceId - 오디오 장치 ID + * @returns stream - MediaStream 객체 + * @returns volume - 현재 오디오 볼륨 (0-255) + * @returns error - 에러 메시지 + * @returns isLoading - 스트림 로딩 중 여부 + * @returns restartStream - 스트림 재시작 함수 + */ import { useCallback, useEffect, useRef, useState } from 'react'; declare global { diff --git a/src/hooks/useProjectList.ts b/src/hooks/useProjectList.ts index e442512d..1ba3ba3a 100644 --- a/src/hooks/useProjectList.ts +++ b/src/hooks/useProjectList.ts @@ -1,31 +1,38 @@ +/** + * 프로젝트 목록 필터/정렬 훅 + * + * 검색, 정렬, 커스텀 필터를 적용한 프로젝트 목록을 반환합니다. + * + * @param projects - 원본 프로젝트 배열 + * @param options.query - 검색어 (제목 기준) + * @param options.sort - 정렬 모드 ('recent' | 'commentCount' | 'name') + * @param options.filterFn - 커스텀 필터 함수 + * @returns 필터링/정렬된 프로젝트 배열 + */ import { useMemo } from 'react'; import type { SortMode } from '@/types/home'; -import type { CardItems } from '@/types/project'; +import type { Project } from '@/types/project'; type Options = { query?: string; sort?: SortMode; - // 필터 조건이 확정되기 전까지는 predicate 형태로 열어두면 - // UI/요구사항이 바뀌어도 훅은 그대로 재사용 가능... - filterFn?: (project: CardItems) => boolean; + filterFn?: (project: Project) => boolean; }; -export function useProjectList(projects: CardItems[], options?: Options) { + +export function useProjectList(projects: Project[], options?: Options) { return useMemo(() => { const query = options?.query ?? ''; const sort = options?.sort ?? 'recent'; const filterFn = options?.filterFn; - // 1) filter let result = filterFn ? projects.filter(filterFn) : projects; - // 2) search const q = query.trim().toLowerCase(); if (q) { result = result.filter((p) => p.title.toLowerCase().includes(q)); } - // 3) sort if (sort === 'recent') return result; const next = [...result]; diff --git a/src/hooks/useReactions.ts b/src/hooks/useReactions.ts index a9f8b717..d8718cf3 100644 --- a/src/hooks/useReactions.ts +++ b/src/hooks/useReactions.ts @@ -1,15 +1,21 @@ -// hooks/useReactions.ts +/** + * 이모지 리액션 훅 + * + * Optimistic UI 패턴으로 리액션 토글을 처리합니다. + * + * @returns reactions - 현재 슬라이드의 리액션 목록 + * @returns toggleReaction - 리액션 토글 함수 + */ import { useSlideStore } from '@/stores/slideStore'; -import type { EmojiReaction, ReactionType } from '@/types/script'; +import type { Reaction, ReactionType } from '@/types/script'; import { showToast } from '@/utils/toast'; import { useToggleReaction } from './queries/useReactionQueries'; -const EMPTY_REACTIONS: EmojiReaction[] = []; +const EMPTY_REACTIONS: Reaction[] = []; export function useReactions() { const slideId = useSlideStore((state) => state.slide?.id); - // SlideStore에서 "현재 슬라이드의 리액션"과 "토글 함수"를 가져옵니다. const reactions = useSlideStore((state) => state.slide?.emojiReactions ?? EMPTY_REACTIONS); const toggleReactionStore = useSlideStore((state) => state.toggleReaction); @@ -18,16 +24,13 @@ export function useReactions() { const toggleReaction = (type: ReactionType) => { if (!slideId) return; - // 1. Optimistic Update (Store) toggleReactionStore(type); - // 2. API Call toggleReactionApi( { slideId, data: { type } }, { onError: () => { showToast.error('반응을 반영하지 못했습니다.'); - // Optimistic Update Rollback toggleReactionStore(type); }, }, diff --git a/src/hooks/useSlideNavigation.ts b/src/hooks/useSlideNavigation.ts index 8be4119e..2a83cad9 100644 --- a/src/hooks/useSlideNavigation.ts +++ b/src/hooks/useSlideNavigation.ts @@ -1,9 +1,10 @@ /** - * @file useSlideNavigation.ts - * @description 슬라이드 네비게이션 훅 + * 슬라이드 네비게이션 훅 * - * 슬라이드 간 이동(이전/다음)과 인덱스 관리를 담당합니다. - * 데이터 fetching과 분리된 순수 네비게이션 로직입니다. + * 슬라이드 간 이동과 인덱스 관리를 담당합니다. + * + * @param totalSlides - 전체 슬라이드 수 + * @param options.initialIndex - 초기 인덱스 (기본값: 0) */ import { useState } from 'react'; @@ -33,9 +34,7 @@ export function useSlideNavigation(totalSlides: number, options: UseSlideNavigat setSlideIndex(index); }; - /** - * 슬라이드 참조 문자열로 이동 (예: "슬라이드 3") - */ + /** 슬라이드 참조 문자열로 이동 (예: "슬라이드 3") */ const goToSlideRef = (slideRef: string) => { const match = slideRef.match(/\d+/); if (!match) return; diff --git a/src/hooks/useSlideSelectors.ts b/src/hooks/useSlideSelectors.ts index 786b9f5d..0049792f 100644 --- a/src/hooks/useSlideSelectors.ts +++ b/src/hooks/useSlideSelectors.ts @@ -1,92 +1,44 @@ /** - * @file useSlideSelectors.ts - * @description 슬라이드 스토어의 커스텀 셀렉터 훅 모음 + * 슬라이드 스토어 셀렉터 훅 * - * Zustand 스토어에서 자주 사용하는 셀렉터들을 커스텀 훅으로 제공합니다. - * 각 훅은 필요한 데이터만 구독하여 불필요한 리렌더링을 방지합니다. - * - * @example - * ```tsx - * // 개별 데이터만 구독 (최적화됨) - * const title = useSlideTitle(); - * const script = useSlideScript(); - * - * // 액션만 필요한 경우 - * const { updateScript, saveToHistory } = useSlideActions(); - * ``` - * - * @see {@link ../stores/slideStore.ts} 원본 스토어 + * 필요한 상태만 구독하여 불필요한 리렌더링을 방지합니다. */ import { useShallow } from 'zustand/shallow'; import { useSlideStore } from '@/stores/slideStore'; -import type { CommentItem } from '@/types/comment'; -import type { EmojiReaction, HistoryItem } from '@/types/script'; +import type { Comment } from '@/types/comment'; +import type { History, Reaction } from '@/types/script'; // 빈 배열 상수 (참조 안정성을 위해) -const EMPTY_OPINIONS: CommentItem[] = []; -const EMPTY_HISTORY: HistoryItem[] = []; -const EMPTY_EMOJIS: EmojiReaction[] = []; +const EMPTY_OPINIONS: Comment[] = []; +const EMPTY_HISTORY: History[] = []; +const EMPTY_EMOJIS: Reaction[] = []; -/** - * 슬라이드 ID를 구독합니다. - * @returns 현재 슬라이드 ID (없으면 빈 문자열) - */ +/** 슬라이드 ID 구독 */ export const useSlideId = () => useSlideStore((state) => state.slide?.id ?? ''); -/** - * 슬라이드 제목을 구독합니다. - * @returns 현재 슬라이드 제목 (없으면 빈 문자열) - */ +/** 슬라이드 제목 구독 */ export const useSlideTitle = () => useSlideStore((state) => state.slide?.title ?? ''); -/** - * 슬라이드 썸네일을 구독합니다. - * @returns 현재 슬라이드 썸네일 URL (없으면 빈 문자열) - */ +/** 슬라이드 썸네일 구독 */ export const useSlideThumb = () => useSlideStore((state) => state.slide?.thumb ?? ''); -/** - * 슬라이드 대본을 구독합니다. - * @returns 현재 슬라이드 대본 (없으면 빈 문자열) - */ +/** 슬라이드 대본 구독 */ export const useSlideScript = () => useSlideStore((state) => state.slide?.script ?? ''); -/** - * 의견 목록을 구독합니다. - * @returns 의견 배열 (없으면 빈 배열) - */ +/** 의견 목록 구독 */ export const useSlideOpinions = () => useSlideStore((state) => state.slide?.opinions ?? EMPTY_OPINIONS); -/** - * 대본 수정 기록을 구독합니다. - * @returns 히스토리 배열 (없으면 빈 배열) - */ +/** 대본 수정 기록 구독 */ export const useSlideHistory = () => useSlideStore((state) => state.slide?.history ?? EMPTY_HISTORY); -/** - * 이모지 반응 목록을 구독합니다. - * @returns 이모지 반응 배열 (없으면 빈 배열) - */ +/** 이모지 반응 목록 구독 */ export const useSlideEmojis = () => useSlideStore((state) => state.slide?.emojiReactions ?? EMPTY_EMOJIS); -/** - * 슬라이드 스토어의 액션들을 반환합니다. - * 액션은 참조가 안정적이므로 리렌더링을 유발하지 않습니다. - * - * @example - * ```tsx - * const { updateScript, saveToHistory, deleteOpinion } = useSlideActions(); - * - * const handleSave = () => { - * saveToHistory(); - * // 저장 완료 알림 - * }; - * ``` - */ +/** 슬라이드 스토어 액션들 (참조 안정적) */ export const useSlideActions = () => useSlideStore( useShallow((state) => ({ diff --git a/src/hooks/useToggle.ts b/src/hooks/useToggle.ts index a8ead198..2e569c8e 100644 --- a/src/hooks/useToggle.ts +++ b/src/hooks/useToggle.ts @@ -1,21 +1,15 @@ import { useCallback, useState } from 'react'; /** - * 불리언 상태 토글을 위한 커스텀 훅 - * @param initial - 초기 상태 값 (기본값: false) - * @returns [value, setValue, toggle, on, off] - * @example - * const { value: isOpen, toggle, on, off } = useToggle(false); + * 불리언 상태 토글 훅 + * + * @param initial - 초기 상태 (기본값: false) + * @returns value, setValue, toggle, on, off */ export const useToggle = (initial = false) => { const [value, setValue] = useState(initial); - // toggle 함수 동일한 참조 유지. - const toggle = useCallback(() => { - setValue((prev) => !prev); - }, []); - - // pop over 바깥 off 클릭시 닫기 구현 + const toggle = useCallback(() => setValue((prev) => !prev), []); const on = useCallback(() => setValue(true), []); const off = useCallback(() => setValue(false), []); diff --git a/src/hooks/useUpload.ts b/src/hooks/useUpload.ts index e22273e1..734604b4 100644 --- a/src/hooks/useUpload.ts +++ b/src/hooks/useUpload.ts @@ -1,3 +1,14 @@ +/** + * 파일 업로드 훅 + * + * 진행률 추적과 에러 처리를 포함한 파일 업로드 기능을 제공합니다. + * + * @returns progress - 업로드 진행률 (0-100) + * @returns state - 업로드 상태 ('idle' | 'uploading' | 'done' | 'error') + * @returns error - 에러 메시지 + * @returns uploadFiles - 파일 업로드 함수 + * @returns reset - 상태 초기화 함수 + */ import { useState } from 'react'; import type { AxiosError } from 'axios'; @@ -6,9 +17,8 @@ import { type ApiError, apiClient } from '@/api/client'; type UploadState = 'idle' | 'uploading' | 'done' | 'error'; -// ✅TODO const UPLOAD_ENDPOINT = '/projects/upload'; -const FORM_KEY = 'files'; // 서버가 file인지 files인지 +const FORM_KEY = 'files'; export function useUpload() { const [progress, setProgress] = useState(0); // 0~100 diff --git a/src/main.tsx b/src/main.tsx index df84da1e..26866e5f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,7 +9,6 @@ import { Toaster } from 'sonner'; import { queryClient } from '@/api'; import { Gnb, Layout, LoginButton, Logo, ShareButton } from '@/components/common'; import { DevFab } from '@/components/common/DevFab'; -import { DEFAULT_SLIDE_ID } from '@/constants/navigation'; import { DevTestPage, FdSlidePage, @@ -52,8 +51,8 @@ const router = createBrowserRouter([ /> ), children: [ - { index: true, element: }, - { path: 'slide/:slideId', element: }, + { index: true, element: }, + { path: 'slide', element: }, { path: 'video', element: }, { path: 'insight', element: }, ], diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index 6afaf39f..b4557c72 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -28,7 +28,8 @@ export const handlers = [ const { projectId } = params; console.log(`[MSW] GET /projects/${projectId}/slides`); - return HttpResponse.json(slides); + const projectSlides = slides.filter((s) => s.projectId === projectId); + return HttpResponse.json(projectSlides); }), /** @@ -104,12 +105,13 @@ export const handlers = [ http.post(`${BASE_URL}/projects/:projectId/slides`, async ({ params, request }) => { await delay(300); - const { projectId } = params; + const { projectId } = params as { projectId: string }; const data = (await request.json()) as { title: string; script?: string }; console.log(`[MSW] POST /projects/${projectId}/slides`, data); const newSlide: Slide = { id: crypto.randomUUID(), + projectId, title: data.title, thumb: `/thumbnails/slide-${slides.length % 52}.webp`, script: data.script || '', diff --git a/src/mocks/projects.ts b/src/mocks/projects.ts index 3227f1a7..b424e027 100644 --- a/src/mocks/projects.ts +++ b/src/mocks/projects.ts @@ -1,105 +1,46 @@ -import type { CardItems } from '@/types/project'; +import type { Project } from '@/types/project'; -const dates = { - daysAgo: (days: number) => { - const d = new Date(); - d.setDate(d.getDate() - days); - const yyyy = d.getFullYear(); - const mm = String(d.getMonth() + 1).padStart(2, '0'); - const dd = String(d.getDate()).padStart(2, '0'); - return `${yyyy}. ${mm}. ${dd}`; - }, -}; +import { daysAgo } from './utils'; -export const MOCK_PROJECTS: CardItems[] = [ +export const MOCK_PROJECTS: Project[] = [ { id: 'p1', - title: 'Q4 마케팅 전략 발표', - updatedAt: dates.daysAgo(1), - pageCount: 11, - commentCount: 0, - reactionCount: 0, + title: '네이버 지도 리브랜딩, 새로운 여정의 시작', + updatedAt: daysAgo(1), + pageCount: 70, + commentCount: 19, + reactionCount: 325, viewCount: 18, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Q4', + thumbnailUrl: '/thumbnails/p1/0.webp', }, { id: 'p2', - title: '신제품 론칭 계획', - updatedAt: dates.daysAgo(2), - pageCount: 8, + title: '당근페이 송금의 플랫폼화: 중고거래 채팅 벗어나기', + updatedAt: daysAgo(2), + pageCount: 59, commentCount: 0, reactionCount: 0, viewCount: 23, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Launch', + thumbnailUrl: '/thumbnails/p2/0.webp', }, { id: 'p3', - title: '2024 연간 실적 리뷰', - updatedAt: dates.daysAgo(3), + title: '강남언니 회사소개서', + updatedAt: daysAgo(6), pageCount: 17, - commentCount: 1, - reactionCount: 2, - viewCount: 94, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Review', - }, - { - id: 'p4', - title: '브랜드 리뉴얼 컨셉 제안서', - updatedAt: dates.daysAgo(5), - pageCount: 14, - commentCount: 3, - reactionCount: 6, - viewCount: 57, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Brand', - }, - { - id: 'p5', - title: '서비스 온보딩 개선안', - updatedAt: dates.daysAgo(6), - pageCount: 9, commentCount: 2, reactionCount: 1, viewCount: 36, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Onboarding', + thumbnailUrl: '/thumbnails/p3/0.webp', }, { - id: 'p6', - title: '시장 경쟁사 분석 보고서', - updatedAt: dates.daysAgo(8), - pageCount: 12, + id: 'p4', + title: '모빌리티 혁신 플랫폼, 소카', + updatedAt: daysAgo(8), + pageCount: 28, commentCount: 4, reactionCount: 3, viewCount: 61, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Market', - }, - { - id: 'p7', - title: 'AI 기능 기획 초안', - updatedAt: dates.daysAgo(9), - pageCount: 6, - commentCount: 0, - reactionCount: 1, - viewCount: 29, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=AI', - }, - { - id: 'p8', - title: '파트너십 제안서', - updatedAt: dates.daysAgo(12), - pageCount: 10, - commentCount: 1, - reactionCount: 0, - viewCount: 41, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Partner', - }, - { - id: 'p9', - title: '2025 로드맵', - updatedAt: dates.daysAgo(15), - pageCount: 20, - commentCount: 5, - reactionCount: 9, - viewCount: 120, - thumbnailUrl: 'https://via.placeholder.com/320x180?text=Roadmap', + thumbnailUrl: '/thumbnails/p4/0.webp', }, ]; diff --git a/src/mocks/slides.ts b/src/mocks/slides.ts index d08ed214..81dace54 100644 --- a/src/mocks/slides.ts +++ b/src/mocks/slides.ts @@ -1,21 +1,7 @@ import type { Slide } from '@/types/slide'; -import dayjs, { type ManipulateType } from '@/utils/dayjs'; import { MOCK_USERS } from './users'; - -/** - * 목 데이터용 타임스탬프 헬퍼 - * - * @example - * ts.ago(2, 'minute') // 2분 전 - * ts.ago(1, 'hour') // 1시간 전 - * ts.at(2, 10, 30) // 2일 전 10:30 - */ -const ts = { - ago: (value: number, unit: ManipulateType) => dayjs().subtract(value, unit).toISOString(), - at: (daysAgo: number, hour: number, minute: number) => - dayjs().subtract(daysAgo, 'day').hour(hour).minute(minute).toISOString(), -}; +import { timeAgo, timeAt } from './utils'; /** * 임시 슬라이드 데이터 @@ -24,12 +10,35 @@ const ts = { * 타임스탬프는 ISO 8601 형식을 사용하며, * UI 레이어에서 상대 시간(dayjs.fromNow) 또는 절대 시간(dayjs.format)으로 변환합니다. */ -export const MOCK_SLIDES: Slide[] = [ + +// 프로젝트별 슬라이드 개수 정의 +const PROJECT_SLIDE_COUNTS = { + p1: 70, + p2: 59, + p3: 17, + p4: 28, +}; + +// 기본 슬라이드 생성 함수 +const createDefaultSlide = (projectId: string, index: number): Slide => ({ + id: `${projectId}-${index}`, // ID 유니크하게 변경 + projectId, + title: `슬라이드 ${index + 1}`, + thumb: `/thumbnails/${projectId}/${index}.webp`, + script: '', + opinions: [], + history: [], + emojiReactions: [], +}); + +// P1 프로젝트의 상세 목 데이터 (기존 데이터 유지) +const p1Slides: Slide[] = [ // 1. 풀 데이터 - 의견+답글+이모지+대본+히스토리 { - id: '1', + id: 'p1-0', + projectId: 'p1', title: '도입', - thumb: '/thumbnails/slide-0.webp', + thumb: '/thumbnails/p1/0.webp', script: '안녕하세요, 오늘 발표를 맡은 김또랑입니다.\n이번 프로젝트는 프레젠테이션 협업 도구입니다.', opinions: [ @@ -37,14 +46,14 @@ export const MOCK_SLIDES: Slide[] = [ id: '1', authorId: MOCK_USERS[1].id, content: '도입부가 인상적이에요!', - timestamp: ts.ago(2, 'minute'), + timestamp: timeAgo(2, 'minute'), isMine: false, }, { id: '2', authorId: MOCK_USERS[0].id, content: '감사합니다~', - timestamp: ts.ago(1, 'minute'), + timestamp: timeAgo(1, 'minute'), isMine: true, isReply: true, parentId: '1', @@ -53,19 +62,19 @@ export const MOCK_SLIDES: Slide[] = [ id: '3', authorId: MOCK_USERS[2].id, content: '첫 문장을 질문으로 시작하면 어떨까요?', - timestamp: ts.ago(30, 'second'), + timestamp: timeAgo(30, 'second'), isMine: false, }, ], history: [ { id: 'h1', - timestamp: ts.at(1, 14, 30), + timestamp: timeAt(1, 14, 30), content: '안녕하세요, 오늘 발표를 맡은 김또랑입니다.', }, { id: 'h2', - timestamp: ts.at(1, 14, 0), + timestamp: timeAt(1, 14, 0), content: '안녕하세요.', }, ], @@ -80,23 +89,24 @@ export const MOCK_SLIDES: Slide[] = [ // 2. 의견 많음 - 스크롤 테스트 { - id: '2', + id: 'p1-1', + projectId: 'p1', title: '문제 정의', - thumb: '/thumbnails/slide-1.webp', + thumb: '/thumbnails/p1/1.webp', script: '', opinions: [ { id: '1', authorId: MOCK_USERS[1].id, content: '문제 정의가 명확하네요', - timestamp: ts.ago(10, 'minute'), + timestamp: timeAgo(10, 'minute'), isMine: false, }, { id: '2', authorId: MOCK_USERS[2].id, content: '동의합니다!', - timestamp: ts.ago(9, 'minute'), + timestamp: timeAgo(9, 'minute'), isMine: false, isReply: true, parentId: '1', @@ -105,14 +115,14 @@ export const MOCK_SLIDES: Slide[] = [ id: '3', authorId: MOCK_USERS[3].id, content: '추가로 이런 문제도 있어요', - timestamp: ts.ago(8, 'minute'), + timestamp: timeAgo(8, 'minute'), isMine: false, }, { id: '4', authorId: MOCK_USERS[0].id, content: '좋은 의견이에요', - timestamp: ts.ago(7, 'minute'), + timestamp: timeAgo(7, 'minute'), isMine: true, isReply: true, parentId: '3', @@ -121,28 +131,28 @@ export const MOCK_SLIDES: Slide[] = [ id: '5', authorId: MOCK_USERS[4].id, content: '사용자 인터뷰 결과도 추가하면 좋겠어요', - timestamp: ts.ago(6, 'minute'), + timestamp: timeAgo(6, 'minute'), isMine: false, }, { id: '6', authorId: MOCK_USERS[1].id, content: '데이터로 뒷받침하면 더 설득력 있을 것 같아요', - timestamp: ts.ago(5, 'minute'), + timestamp: timeAgo(5, 'minute'), isMine: false, }, { id: '7', authorId: MOCK_USERS[2].id, content: '경쟁사 분석도 넣어보는 건 어떨까요?', - timestamp: ts.ago(4, 'minute'), + timestamp: timeAgo(4, 'minute'), isMine: false, }, { id: '8', authorId: MOCK_USERS[0].id, content: '네, 반영해볼게요!', - timestamp: ts.ago(3, 'minute'), + timestamp: timeAgo(3, 'minute'), isMine: true, isReply: true, parentId: '7', @@ -160,43 +170,44 @@ export const MOCK_SLIDES: Slide[] = [ // 3. 히스토리 많음 - 스크롤 테스트 { - id: '3', + id: 'p1-2', + projectId: 'p1', title: '문제 분석', - thumb: '/thumbnails/slide-2.webp', + thumb: '/thumbnails/p1/2.webp', script: '문제의 근본 원인은 세 가지로 분류할 수 있습니다.\n첫째, 기능적 한계입니다.\n둘째, 구조적 문제입니다.\n셋째, 사용 흐름의 복잡성입니다.', opinions: [], history: [ { id: 'h1', - timestamp: ts.at(2, 10, 30), + timestamp: timeAt(2, 10, 30), content: '문제의 근본 원인은 세 가지로 분류할 수 있습니다.\n첫째, 기능적 한계입니다.\n둘째, 구조적 문제입니다.\n셋째, 사용 흐름의 복잡성입니다.', }, { id: 'h2', - timestamp: ts.at(2, 10, 15), + timestamp: timeAt(2, 10, 15), content: '문제의 근본 원인은 세 가지로 분류할 수 있습니다.\n첫째, 기능적 한계입니다.\n둘째, 구조적 문제입니다.', }, { id: 'h3', - timestamp: ts.at(2, 10, 0), + timestamp: timeAt(2, 10, 0), content: '문제의 근본 원인은 세 가지로 분류할 수 있습니다.\n첫째, 기능적 한계입니다.', }, { id: 'h4', - timestamp: ts.at(2, 9, 45), + timestamp: timeAt(2, 9, 45), content: '문제의 근본 원인은 세 가지로 분류할 수 있습니다.', }, { id: 'h5', - timestamp: ts.at(2, 9, 30), + timestamp: timeAt(2, 9, 30), content: '문제의 원인을 분석해보겠습니다.', }, { id: 'h6', - timestamp: ts.at(3, 18, 0), + timestamp: timeAt(3, 18, 0), content: '문제 분석 초안입니다.', }, ], @@ -211,16 +222,17 @@ export const MOCK_SLIDES: Slide[] = [ // 4. 이모지 많음 - 더보기 팝오버 테스트 { - id: '4', + id: 'p1-3', + projectId: 'p1', title: '해결 목표', - thumb: '/thumbnails/slide-3.webp', + thumb: '/thumbnails/p1/3.webp', script: '', opinions: [ { id: '1', authorId: MOCK_USERS[3].id, content: '목표가 명확해요!', - timestamp: ts.ago(1, 'hour'), + timestamp: timeAgo(1, 'hour'), isMine: false, }, ], @@ -236,15 +248,16 @@ export const MOCK_SLIDES: Slide[] = [ // 5. 이모지 99+ - 카운트 표시 테스트 { - id: '5', + id: 'p1-4', + projectId: 'p1', title: '해결 방안', - thumb: '/thumbnails/slide-4.webp', + thumb: '/thumbnails/p1/4.webp', script: '핵심 해결 방안은 다음과 같습니다.', opinions: [], history: [ { id: 'h1', - timestamp: ts.at(2, 11, 0), + timestamp: timeAt(2, 11, 0), content: '핵심 해결 방안은 다음과 같습니다.', }, ], @@ -259,16 +272,17 @@ export const MOCK_SLIDES: Slide[] = [ // 6. 긴 제목 - truncate 테스트 { - id: '6', + id: 'p1-5', + projectId: 'p1', title: '기능 구성 및 상세 설계 - 핵심 모듈 분석', - thumb: '/thumbnails/slide-5.webp', + thumb: '/thumbnails/p1/5.webp', script: '', opinions: [ { id: '1', authorId: MOCK_USERS[4].id, content: '기능 정의가 잘 되어있네요', - timestamp: ts.ago(2, 'hour'), + timestamp: timeAgo(2, 'hour'), isMine: false, }, ], @@ -284,9 +298,10 @@ export const MOCK_SLIDES: Slide[] = [ // 7. 긴 대본 - 스크롤 테스트 { - id: '7', + id: 'p1-6', + projectId: 'p1', title: '화면 흐름', - thumb: '/thumbnails/slide-6.webp', + thumb: '/thumbnails/p1/6.webp', script: `사용자 화면 흐름을 설명드리겠습니다. 1. 로그인 화면 @@ -312,7 +327,7 @@ Google, Kakao, Naver 로그인을 지원합니다. history: [ { id: 'h1', - timestamp: ts.at(2, 15, 0), + timestamp: timeAt(2, 15, 0), content: '사용자 화면 흐름 초안', }, ], @@ -327,23 +342,24 @@ Google, Kakao, Naver 로그인을 지원합니다. // 8. 내 의견만 - 삭제 버튼 테스트 { - id: '8', + id: 'p1-7', + projectId: 'p1', title: '기술적 구현', - thumb: '/thumbnails/slide-7.webp', + thumb: '/thumbnails/p1/7.webp', script: 'React 19, TypeScript, Zustand를 사용합니다.', opinions: [ { id: '1', authorId: MOCK_USERS[0].id, content: 'Zustand로 상태 관리하면 좋을 것 같아요', - timestamp: ts.ago(3, 'hour'), + timestamp: timeAgo(3, 'hour'), isMine: true, }, { id: '2', authorId: MOCK_USERS[0].id, content: 'Context보다 성능이 좋습니다', - timestamp: ts.ago(2, 'hour'), + timestamp: timeAgo(2, 'hour'), isMine: true, isReply: true, parentId: '1', @@ -352,7 +368,7 @@ Google, Kakao, Naver 로그인을 지원합니다. id: '3', authorId: MOCK_USERS[0].id, content: 'Selector 패턴으로 최적화 가능해요', - timestamp: ts.ago(1, 'hour'), + timestamp: timeAgo(1, 'hour'), isMine: true, }, ], @@ -368,30 +384,31 @@ Google, Kakao, Naver 로그인을 지원합니다. // 9. 타인 의견만 - 답글 테스트 { - id: '9', + id: 'p1-8', + projectId: 'p1', title: '기대 효과', - thumb: '/thumbnails/slide-8.webp', + thumb: '/thumbnails/p1/8.webp', script: '', opinions: [ { id: '1', authorId: MOCK_USERS[1].id, content: '기대 효과가 구체적이에요', - timestamp: ts.ago(4, 'hour'), + timestamp: timeAgo(4, 'hour'), isMine: false, }, { id: '2', authorId: MOCK_USERS[2].id, content: '수치화된 목표가 있으면 더 좋겠어요', - timestamp: ts.ago(3, 'hour'), + timestamp: timeAgo(3, 'hour'), isMine: false, }, { id: '3', authorId: MOCK_USERS[3].id, content: '비즈니스 임팩트도 추가해주세요', - timestamp: ts.ago(2, 'hour'), + timestamp: timeAgo(2, 'hour'), isMine: false, }, ], @@ -407,9 +424,10 @@ Google, Kakao, Naver 로그인을 지원합니다. // 10. 빈 데이터 - empty state 테스트 { - id: '10', + id: 'p1-9', + projectId: 'p1', title: '결론', - thumb: '/thumbnails/slide-9.webp', + thumb: '/thumbnails/p1/9.webp', script: '', opinions: [], history: [], @@ -422,3 +440,13 @@ Google, Kakao, Naver 로그인을 지원합니다. ], }, ]; + +// 나머지 프로젝트 및 p1의 나머지 슬라이드 생성 +const generatedSlides = Object.entries(PROJECT_SLIDE_COUNTS).flatMap(([projectId, count]) => { + const startIdx = projectId === 'p1' ? 10 : 0; + return Array.from({ length: count - startIdx }).map((_, i) => + createDefaultSlide(projectId, i + startIdx), + ); +}); + +export const MOCK_SLIDES: Slide[] = [...p1Slides, ...generatedSlides]; diff --git a/src/mocks/utils.ts b/src/mocks/utils.ts new file mode 100644 index 00000000..dbc8d793 --- /dev/null +++ b/src/mocks/utils.ts @@ -0,0 +1,24 @@ +import dayjs, { type ManipulateType } from '@/utils/dayjs'; + +/** + * n일 전 날짜를 ISO 문자열로 반환 + * @param days - 며칠 전인지 (예: 1) + */ +export const daysAgo = (days: number) => dayjs().subtract(days, 'day').toISOString(); + +/** + * n(unit) 전 날짜를 ISO 문자열로 반환 + * @param value - 값 (예: 2) + * @param unit - 단위 (예: 'minute', 'hour') + */ +export const timeAgo = (value: number, unit: ManipulateType) => + dayjs().subtract(value, unit).toISOString(); + +/** + * n일 전 특정 시간의 날짜를 ISO 문자열로 반환 + * @param daysAgo - 며칠 전인지 + * @param hour - 시 (0-23) + * @param minute - 분 (0-59) + */ +export const timeAt = (daysAgo: number, hour: number, minute: number) => + dayjs().subtract(daysAgo, 'day').hour(hour).minute(minute).toISOString(); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 553baa53..10616629 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -3,7 +3,12 @@ import { useEffect, useState } from 'react'; import IntroSection from '@/components/home/IntroSection'; import ProjectsSection from '@/components/home/ProjectsSection'; import { useDebounce } from '@/hooks/useDebounce'; -import { useHomeActions, useHomeQuery } from '@/hooks/useHomeSelectors'; +import { + useHomeActions, + useHomeQuery, + useHomeSort, + useHomeViewMode, +} from '@/hooks/useHomeSelectors'; import { useProjectList } from '@/hooks/useProjectList'; import { useUpload } from '@/hooks/useUpload'; import { MOCK_PROJECTS } from '@/mocks/projects'; @@ -14,11 +19,13 @@ export default function HomePage() { const { progress, state, error, uploadFiles } = useUpload(); const [isLoading, setIsLoading] = useState(true); const query = useHomeQuery(); - const { setQuery } = useHomeActions(); + const sort = useHomeSort(); + const viewMode = useHomeViewMode(); + const { setQuery, setSort, setViewMode } = useHomeActions(); const debouncedQuery = useDebounce(query, 300); // TODO : 나중에 mock_projects 말고 서버데이터로 바꿔주기.. - const projects = useProjectList(MOCK_PROJECTS, { query: debouncedQuery }); + const projects = useProjectList(MOCK_PROJECTS, { query: debouncedQuery, sort }); const isEmpty = !isLoading && MOCK_PROJECTS.length === 0; // TODO : 실제 데이터 패칭 훅의 isLoading으로 교체 @@ -45,6 +52,9 @@ export default function HomePage() { isLoading={isLoading} query={query} onChangeQuery={setQuery} + onChangeSort={setSort} + viewMode={viewMode} + onChangeViewMode={setViewMode} projects={projects} /> diff --git a/src/pages/SlidePage.tsx b/src/pages/SlidePage.tsx index ecc43fdb..c2ed8307 100644 --- a/src/pages/SlidePage.tsx +++ b/src/pages/SlidePage.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useSearchParams } from 'react-router-dom'; import { SlideList, SlideWorkspace } from '@/components/slide'; import { setLastSlideId } from '@/constants/navigation'; @@ -7,13 +7,14 @@ import { useSlides } from '@/hooks/queries/useSlides'; import { showToast } from '@/utils/toast'; export default function SlidePage() { - const { projectId, slideId } = useParams<{ - projectId: string; - slideId: string; - }>(); + const { projectId } = useParams<{ projectId: string }>(); + const [searchParams, setSearchParams] = useSearchParams(); const { data: slides, isLoading, isError } = useSlides(projectId ?? ''); + const slideIdParam = searchParams.get('slideId'); + const currentSlide = slides?.find((s) => s.id === slideIdParam) ?? slides?.[0]; + /** * 슬라이드 로드 에러 처리 */ @@ -23,28 +24,35 @@ export default function SlidePage() { } }, [isError]); + /** + * URL에 slideId가 없거나 유효하지 않은 경우, 첫 번째 슬라이드(또는 기본값)로 리다이렉트 (replace) + */ + useEffect(() => { + if (!isLoading && slides && slides.length > 0) { + if (!slideIdParam) { + // slideId가 아예 없으면 첫 번째 슬라이드로 + setSearchParams({ slideId: slides[0].id }, { replace: true }); + } else if (!slides.find((s) => s.id === slideIdParam)) { + // slideId가 있지만 목록에 없으면 첫 번째 슬라이드로 + setSearchParams({ slideId: slides[0].id }, { replace: true }); + } + } + }, [isLoading, slides, slideIdParam, setSearchParams]); + /** * 탭 이동(영상/인사이트 → 슬라이드) 후 다시 돌아왔을 때 * 마지막으로 보던 슬라이드로 복원하기 위함 */ useEffect(() => { - if (projectId && slideId) { - setLastSlideId(projectId, slideId); + if (projectId && currentSlide?.id) { + setLastSlideId(projectId, currentSlide.id); } - }, [projectId, slideId]); - - const currentSlide = slides?.find((s) => s.id === slideId) ?? slides?.[0]; - const basePath = projectId ? `/${projectId}` : ''; + }, [projectId, currentSlide?.id]); return (
- +
diff --git a/src/pages/dev-test/sections/FeedbackTestSection.tsx b/src/pages/dev-test/sections/FeedbackTestSection.tsx index 2334f417..60ee31d5 100644 --- a/src/pages/dev-test/sections/FeedbackTestSection.tsx +++ b/src/pages/dev-test/sections/FeedbackTestSection.tsx @@ -2,12 +2,12 @@ import { useState } from 'react'; import { CommentInput } from '@/components/comment'; import ReactionButtons from '@/components/feedback/ReactionButtons'; -import { type EmojiReaction, type ReactionType } from '@/types/script'; +import { type Reaction, type ReactionType } from '@/types/script'; import { showToast } from '@/utils/toast'; export function FeedbackTestSection() { const [commentDraft, setCommentDraft] = useState(''); - const [reactions, setReactions] = useState([ + const [reactions, setReactions] = useState([ { type: 'fire', count: 8 }, { type: 'sleepy', count: 4 }, { type: 'good', count: 99, active: true }, diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index b4453855..bdff2ae1 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -1,3 +1,9 @@ +/** + * 인증 상태 관리 스토어 + * + * 사용자 정보, 토큰, 로그인 모달 상태를 관리합니다. + * persist 미들웨어로 브라우저 세션 유지를 지원합니다. + */ import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; @@ -6,13 +12,11 @@ import type { User } from '@/types/auth'; interface AuthState { user: User | null; accessToken: string | null; + isLoginModalOpen: boolean; login: (user: User, accessToken: string) => void; logout: () => void; updateUser: (user: Partial) => void; - - // 로그인 모달 상태/액션 추가 - isLoginModalOpen: boolean; openLoginModal: () => void; closeLoginModal: () => void; } diff --git a/src/stores/homeStore.ts b/src/stores/homeStore.ts index 6b1fafca..db63a5b4 100644 --- a/src/stores/homeStore.ts +++ b/src/stores/homeStore.ts @@ -1,9 +1,23 @@ +/** + * 홈 화면 UI 상태 관리 스토어 + * + * 프로젝트 검색, 정렬, 보기 모드를 관리합니다. + */ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import type { HomeStateProps } from '@/types/home'; +import type { SortMode, ViewMode } from '@/types/home'; -export const useHomeStore = create()( +interface HomeState { + query: string; + viewMode: ViewMode; + sort: SortMode; + setQuery: (query: string) => void; + setViewMode: (viewMode: ViewMode) => void; + setSort: (sort: SortMode) => void; +} + +export const useHomeStore = create()( devtools( (set) => ({ query: '', diff --git a/src/stores/shareStore.ts b/src/stores/shareStore.ts index 2bff5541..aa90083a 100644 --- a/src/stores/shareStore.ts +++ b/src/stores/shareStore.ts @@ -1,58 +1,33 @@ -// src/stores/shareStore.ts -import { create } from 'zustand'; - /** - * 공유 모달에서 사용할 '공유 타입' - * - slide_script: "슬라이드 + 대본" - * - slide_script_video: "슬라이드 + 대본 + 영상" + * 공유 모달 상태 관리 스토어 + * + * 슬라이드/대본/영상 공유 워크플로우를 관리합니다. + * form → result 단계로 진행되며, 공유 링크를 생성합니다. */ +import { create } from 'zustand'; + export type ShareType = 'slide_script' | 'slide_script_video'; -// ShareStoreState는 "공유 모달의 전역 상태"를 정의 interface ShareStoreState { - // 모달이 열려있는지 여부 (LoginModal의 isLoginModalOpen과 동일한 역할) isShareModalOpen: boolean; - - // 모달 내부 단계 - // - form: 공유 옵션 선택/영상 선택/링크 생성 버튼이 보이는 단계 - // - result: 링크 생성이 끝나고 링크+SNS 공유 화면이 보이는 단계 step: 'form' | 'result'; - - // 현재 선택된 공유 유형 shareType: ShareType; - - // 공유할 녹화 영상 id (공유 유형이 영상 포함일 때만 의미 있음) selectedVideoId: string | null; - - // 생성된 공유 링크 shareUrl: string; - // actions (상태 변경 함수들) openShareModal: () => void; closeShareModal: () => void; - setShareType: (type: ShareType) => void; setSelectedVideoId: (videoId: string | null) => void; - - // "공유 링크 생성"을 눌렀을 때 실행할 함수 - // 실제 서비스라면 API 요청해서 링크를 받아오겠지만, - // 지금 시점에서는 임시 토큰을 생성한다. generateShareLink: (params: { projectId: string; shareType: ShareType; - selectedVideoId: string | null; // 첫번째 옵션일땐 비디오 없으니까 null + selectedVideoId: string | null; }) => void; - - // 결과 화면에서 "닫기" 또는 다시 시작 등을 위한 초기화 resetForm: () => void; } -/** - * useShareStore라는 "전역 store 훅" 만들기 - * 상태가 바뀌면, 그 상태를 구독(useShareStore로 읽는)하는 컴포넌트만 다시 렌더된다. - */ export const useShareStore = create((set, get) => ({ - // 기본값 세팅 isShareModalOpen: false, step: 'form', shareType: 'slide_script', @@ -60,8 +35,6 @@ export const useShareStore = create((set, get) => ({ shareUrl: '', openShareModal: () => { - // 모달을 열 때는 form 단계로 보여주는 게 자연스럽다 - // (이전 결과 화면이 남아있지 않게) set({ isShareModalOpen: true, step: 'form', @@ -70,13 +43,12 @@ export const useShareStore = create((set, get) => ({ }, closeShareModal: () => { - // 닫을 때는 모달만 닫고, resetForm은 애니메이션 완료 후 호출 - set({ isShareModalOpen: false }); + set({ + isShareModalOpen: false, + }); }, setShareType: (type) => { - // 공유 유형을 바꾸면, - // 영상 포함이 아닌데 selectedVideoId가 남아있으면 혼란이 생길 수 있다. set({ shareType: type, selectedVideoId: type === 'slide_script_video' ? get().selectedVideoId : null, @@ -86,11 +58,9 @@ export const useShareStore = create((set, get) => ({ setSelectedVideoId: (videoId) => set({ selectedVideoId: videoId }), generateShareLink: ({ projectId, shareType, selectedVideoId }) => { - // 여기서는 임시로 토큰 생성 (실서비스에서는 API 응답값을 사용) - const token = Math.random().toString(36).slice(2, 11); // 간단 토큰 - const base = 'https://ttorang.app/share'; // 시안 형태 + const token = Math.random().toString(36).slice(2, 11); + const base = 'https://ttorang.app/share'; - // shareType/영상 정보를 링크에 쿼리로 넣고 싶다면 이렇게 구성 가능 const query = new URLSearchParams(); query.set('projectId', projectId); query.set('type', shareType); diff --git a/src/stores/slideStore.ts b/src/stores/slideStore.ts index 0491f25c..48539906 100644 --- a/src/stores/slideStore.ts +++ b/src/stores/slideStore.ts @@ -1,102 +1,33 @@ /** - * @file slideStore.ts - * @description 슬라이드 상태 관리 Zustand 스토어 + * 슬라이드 상태 관리 스토어 * * 슬라이드별 대본, 의견, 히스토리, 이모지 반응을 관리합니다. - * 셀렉터 패턴을 사용하여 필요한 데이터만 구독할 수 있습니다. - * - * @example 기본 사용법 - * ```tsx - * // 컴포넌트에서 셀렉터로 구독 (권장) - * const title = useSlideStore((s) => s.slide?.title ?? ''); - * const updateScript = useSlideStore((s) => s.updateScript); - * - * // 또는 커스텀 훅 사용 (더 간편) - * import { useSlideTitle, useSlideActions } from '@/hooks/useSlideSelectors'; - * const title = useSlideTitle(); - * const { updateScript } = useSlideActions(); - * ``` - * - * @example 슬라이드 초기화 (SlideWorkspace에서) - * ```tsx - * const initSlide = useSlideStore((s) => s.initSlide); - * - * useEffect(() => { - * initSlide(slide); - * }, [slide, initSlide]); - * ``` + * 셀렉터 패턴으로 필요한 데이터만 구독할 수 있습니다. * * @see {@link ../hooks/useSlideSelectors.ts} 커스텀 셀렉터 훅 - * @see {@link ../types/slide.ts} Slide 타입 정의 */ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { MOCK_CURRENT_USER } from '@/mocks/users'; -import type { CommentItem } from '@/types/comment'; -import type { HistoryItem, ReactionType } from '@/types/script'; +import type { Comment } from '@/types/comment'; +import type { History, ReactionType } from '@/types/script'; import type { Slide } from '@/types/slide'; import { addReplyToFlat, createComment, deleteFromFlat } from '@/utils/comment'; interface SlideState { slide: Slide | null; - /** - * 슬라이드 데이터를 초기화합니다. - * @param slide - 초기화할 슬라이드 데이터 - */ initSlide: (slide: Slide) => void; - - /** - * 슬라이드 데이터를 부분 업데이트합니다. - * @param updates - 업데이트할 필드들 - */ updateSlide: (updates: Partial) => void; - - /** - * 대본 내용을 업데이트합니다. - * @param script - 새로운 대본 내용 - */ updateScript: (script: string) => void; - - /** - * 현재 대본을 히스토리에 저장합니다. - * 대본이 비어있으면 저장하지 않습니다. - */ saveToHistory: () => void; - - /** - * 특정 시점의 대본으로 복원합니다. - * @param item - 복원할 히스토리 아이템 - */ - restoreFromHistory: (item: HistoryItem) => void; - - /** - * 의견을 삭제합니다. - * 해당 의견의 대댓글도 함께 제거됩니다. - * @param id - 삭제할 의견 ID - */ + restoreFromHistory: (item: History) => void; deleteOpinion: (id: string) => void; - - /** - * 의견에 답글을 추가합니다. - * @param parentId - 원본 의견 ID - * @param content - 답글 내용 - */ addReply: (parentId: string, content: string) => void; - - // 이모지 토글 액션을 정의합니다. toggleReaction: (type: ReactionType) => void; - - // 새 루트 댓글 작성 액션 addOpinion: (content: string, slideIndex: number) => void; - - /** - * 의견 목록을 통째로 교체합니다. - * (Optimistic UI 롤백용) - * @param opinions - 교체할 의견 목록 - */ - setOpinions: (opinions: CommentItem[]) => void; + setOpinions: (opinions: Comment[]) => void; } export const useSlideStore = create()( @@ -131,16 +62,14 @@ export const useSlideStore = create()( saveToHistory: () => { set( (state) => { - // 슬라이드가 없거나 대본이 비어있으면 저장하지 않음 if (!state.slide || !state.slide.script.trim()) return state; - const historyItem: HistoryItem = { + const historyItem: History = { id: crypto.randomUUID(), timestamp: new Date().toISOString(), content: state.slide.script, }; - // 새 히스토리를 맨 앞에 추가 (최신순 정렬) return { slide: { ...state.slide, @@ -201,20 +130,15 @@ export const useSlideStore = create()( toggleReaction: (type) => { set( (state) => { - // 슬라이드가 없거나 리액션 배열이 없으면 무시 if (!state.slide) return state; const currentReactions = state.slide.emojiReactions || []; - - // 해당 타입 찾아서 업데이트 const newReactions = currentReactions.map((r) => { if (r.type !== type) return r; - // 활성 -> 비활성 (카운트 감소) if (r.active) { return { ...r, active: false, count: Math.max(0, r.count - 1) }; } - // 비활성 -> 활성 (카운트 증가) return { ...r, active: true, count: r.count + 1 }; }); @@ -226,7 +150,7 @@ export const useSlideStore = create()( }; }, false, - 'slide/toggleReaction', // DevTools 액션 이름 + 'slide/toggleReaction', ); }, @@ -236,7 +160,7 @@ export const useSlideStore = create()( const newComment = createComment({ content: trimmed, - authorId: MOCK_CURRENT_USER.id, // 혹은 로그인된 유저 정보 + authorId: MOCK_CURRENT_USER.id, slideRef: `슬라이드 ${slideIndex + 1}`, }); diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts index dd66d49d..be21e57e 100644 --- a/src/stores/themeStore.ts +++ b/src/stores/themeStore.ts @@ -1,3 +1,9 @@ +/** + * 테마 관리 스토어 + * + * light/dark/auto 테마 설정을 관리하고 시스템 설정과 동기화합니다. + * persist 미들웨어로 사용자 선호도를 저장합니다. + */ import { useEffect } from 'react'; import { create } from 'zustand'; @@ -21,11 +27,11 @@ export const useThemeStore = create()( persist( (set, get) => ({ theme: 'auto', - resolvedTheme: 'light', // 초기값, initTheme에서 보정됨 + resolvedTheme: 'light', setTheme: (theme) => { set({ theme }); - get().initTheme(); // 테마 변경 시 재계산 및 적용 + get().initTheme(); }, initTheme: () => { @@ -40,9 +46,6 @@ export const useThemeStore = create()( set({ resolvedTheme: nextResolved }); document.documentElement.dataset.theme = nextResolved; - - // 시스템 설정 변경 감지 리스너 (한 번만 등록되도록 관리 필요하지만, 간단히 구현) - // Zustand persist가 hydarate 될 때 실행되거나, 앱 진입 시 실행됨. }, }), { @@ -55,13 +58,13 @@ export const useThemeStore = create()( ); /** - * 시스템 테마 변경 및 스토리지 동기화 리스너 훅 - * 앱의 최상위 컴포넌트에서 한 번만 호출해야 합니다. + * 시스템 테마 변경 및 탭 간 동기화 리스너 훅 + * + * 앱 최상위 컴포넌트에서 한 번만 호출해야 합니다. */ export function useThemeListener() { const initTheme = useThemeStore((state) => state.initTheme); - // 시스템 테마 변경 감지 useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = () => { @@ -74,7 +77,6 @@ export function useThemeListener() { return () => mediaQuery.removeEventListener('change', handleChange); }, [initTheme]); - // 탭 간 테마 동기화 (Storage 이벤트 감지) useEffect(() => { const handleStorage = (e: StorageEvent) => { if (e.key === 'ttorang-theme') { diff --git a/src/types/comment.ts b/src/types/comment.ts index 1c0ef565..56a8b692 100644 --- a/src/types/comment.ts +++ b/src/types/comment.ts @@ -1,4 +1,4 @@ -export interface CommentItem { +export interface Comment { id: string; authorId: string; content: string; @@ -13,7 +13,7 @@ export interface CommentItem { /** 부모 댓글 ID - 플랫 구조에서 답글 관계 표현 */ parentId?: string; /** 자식 댓글 목록 - 중첩 구조에서 답글 관계 표현 */ - replies?: CommentItem[]; + replies?: Comment[]; } /** diff --git a/src/types/home.ts b/src/types/home.ts index 6e0d140a..dacf46f0 100644 --- a/src/types/home.ts +++ b/src/types/home.ts @@ -1,24 +1,3 @@ -import type { UploadState } from './uploadFile'; - export type ViewMode = 'card' | 'list'; export type SortMode = 'recent' | 'commentCount' | 'name'; - -export interface IntroSectionProps { - accept: string; - disabled: boolean; - uploadState: UploadState; - progress: number; - error?: string | null; - onFilesSelected: (files: File[]) => void; - isEmpty: boolean; -} - -export interface HomeStateProps { - query: string; - viewMode: ViewMode; - sort: SortMode; - setQuery: (query: string) => void; - setViewMode: (viewMode: ViewMode) => void; - setSort: (sort: SortMode) => void; -} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..4ed8707d --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,26 @@ +// Auth +export type { AuthProvider, User } from './auth'; + +// Comment +export type { Comment, CreateCommentInput } from './comment'; + +// Home +export type { SortMode, ViewMode } from './home'; + +// Navigation +export type { TabItem, TabKey } from './navigation'; + +// Project +export type { Project } from './project'; + +// Script +export type { History, Reaction, ReactionType } from './script'; + +// Slide +export type { Slide } from './slide'; + +// Theme +export type { ThemeMode } from './theme'; + +// Upload +export type { UploadState } from './uploadFile'; diff --git a/src/types/project.ts b/src/types/project.ts index fb85ba73..7bba7c41 100644 --- a/src/types/project.ts +++ b/src/types/project.ts @@ -1,10 +1,4 @@ -export interface ProjectHeaderProps { - value: string; - onChange: (value: string) => void; -} - -// 프로젝트 전체의 코멘트 수, 리액션 수 => 슬라이드와 연결할 것.. -export interface CardItems { +export interface Project { id: string; title: string; updatedAt: string; diff --git a/src/types/script.ts b/src/types/script.ts index d98887a3..e3a81957 100644 --- a/src/types/script.ts +++ b/src/types/script.ts @@ -1,41 +1,22 @@ /** * 대본 수정 기록 아이템 */ -export interface HistoryItem { +export interface History { id: string; - /** @format "M월 D일 HH:mm" */ timestamp: string; content: string; } /** - * 리액션 타입 (5가지 고정) + * 리액션 타입 */ export type ReactionType = 'fire' | 'sleepy' | 'good' | 'bad' | 'confused'; -/** - * 리액션 설정 (이모지, 라벨 매핑) - */ -export const REACTION_CONFIG: Record = { - fire: { emoji: '🔥', label: '인상적이에요' }, - sleepy: { emoji: '💤', label: '지루해요' }, - good: { emoji: '👍', label: '잘했어요' }, - bad: { emoji: '👎', label: '별로에요' }, - confused: { emoji: '🤷', label: '이해 안돼요' }, -} as const; - -/** - * 리액션 타입 목록 (순서 보장) - */ -export const REACTION_TYPES: ReactionType[] = ['fire', 'sleepy', 'good', 'bad', 'confused']; - /** * 이모지 반응 정보 */ -export interface EmojiReaction { +export interface Reaction { type: ReactionType; - /** 99 초과 시 "99+"로 표시 */ count: number; - /** 활성화 여부 (UI용) */ active?: boolean; } diff --git a/src/types/slide.ts b/src/types/slide.ts index 0e34d7ea..ebbe71fc 100644 --- a/src/types/slide.ts +++ b/src/types/slide.ts @@ -1,5 +1,5 @@ -import type { CommentItem } from './comment'; -import type { EmojiReaction, HistoryItem } from './script'; +import type { Comment } from './comment'; +import type { History, Reaction } from './script'; /** * 슬라이드 데이터 모델 @@ -9,12 +9,11 @@ import type { EmojiReaction, HistoryItem } from './script'; */ export interface Slide { id: string; + projectId: string; title: string; thumb: string; - /** 포커스 해제 시 히스토리에 자동 저장 */ script: string; - opinions: CommentItem[]; - /** 최신순 정렬 */ - history: HistoryItem[]; - emojiReactions: EmojiReaction[]; + opinions: Comment[]; + history: History[]; + emojiReactions: Reaction[]; } diff --git a/src/types/uploadFile.ts b/src/types/uploadFile.ts index 505a3a9b..60edd57e 100644 --- a/src/types/uploadFile.ts +++ b/src/types/uploadFile.ts @@ -1,9 +1 @@ export type UploadState = 'idle' | 'uploading' | 'done' | 'error'; - -export interface FileDropProps { - onFilesSelected: (files: File[]) => void; - accept?: string; - disabled?: boolean; - uploadState?: UploadState; - progress?: number; // 0~100 -} diff --git a/src/utils/comment.ts b/src/utils/comment.ts index 7fd45b5a..b04f60a1 100644 --- a/src/utils/comment.ts +++ b/src/utils/comment.ts @@ -1,4 +1,4 @@ -import type { CommentItem, CreateCommentInput } from '@/types/comment'; +import type { Comment, CreateCommentInput } from '@/types/comment'; /** * 고유 ID 생성 @@ -10,7 +10,7 @@ export function generateCommentId(): string { /** * 새 댓글 객체 생성 */ -export function createComment(input: CreateCommentInput): CommentItem { +export function createComment(input: CreateCommentInput): Comment { const isReply = Boolean(input.parentId); return { @@ -32,10 +32,10 @@ export function createComment(input: CreateCommentInput): CommentItem { * SlidePage(Opinion) 방식: 플랫 배열 + parentId 참조 */ export function addReplyToFlat( - comments: CommentItem[], + comments: Comment[], parentId: string, input: Omit, -): CommentItem[] { +): Comment[] { const newReply = createComment({ ...input, parentId }); const parentIndex = comments.findIndex((c) => c.id === parentId); @@ -52,13 +52,13 @@ export function addReplyToFlat( * FeedbackSlidePage(Comment) 방식: 중첩 배열 구조 */ export function addReplyToTree( - comments: CommentItem[], + comments: Comment[], parentId: string, input: Omit, -): CommentItem[] { +): Comment[] { const newReply = createComment({ ...input, parentId }); - const addToTree = (list: CommentItem[]): CommentItem[] => { + const addToTree = (list: Comment[]): Comment[] => { return list.map((node) => { if (node.id === parentId) { return { ...node, replies: [...(node.replies ?? []), newReply] }; @@ -76,15 +76,15 @@ export function addReplyToTree( /** * 플랫 배열에서 댓글 삭제 (부모 삭제 시 자식도 함께 삭제) */ -export function deleteFromFlat(comments: CommentItem[], targetId: string): CommentItem[] { +export function deleteFromFlat(comments: Comment[], targetId: string): Comment[] { return comments.filter((c) => c.id !== targetId && c.parentId !== targetId); } /** * 중첩 배열에서 댓글 삭제 */ -export function deleteFromTree(comments: CommentItem[], targetId: string): CommentItem[] { - const removeFromTree = (list: CommentItem[]): CommentItem[] => { +export function deleteFromTree(comments: Comment[], targetId: string): Comment[] { + const removeFromTree = (list: Comment[]): Comment[] => { return list .filter((node) => node.id !== targetId) .map((node) => { @@ -103,9 +103,9 @@ export function deleteFromTree(comments: CommentItem[], targetId: string): Comme * * parentId를 기반으로 replies 중첩 구조로 변환합니다. */ -export function flatToTree(comments: CommentItem[]): CommentItem[] { - const map = new Map(); - const roots: CommentItem[] = []; +export function flatToTree(comments: Comment[]): Comment[] { + const map = new Map(); + const roots: Comment[] = []; // 1. 모든 댓글을 맵에 저장 (replies 초기화) for (const comment of comments) { @@ -133,10 +133,10 @@ export function flatToTree(comments: CommentItem[]): CommentItem[] { * * 부모 바로 다음에 자식들이 오도록 순서 보장합니다. */ -export function treeToFlat(comments: CommentItem[]): CommentItem[] { - const result: CommentItem[] = []; +export function treeToFlat(comments: Comment[]): Comment[] { + const result: Comment[] = []; - const flatten = (list: CommentItem[], parentId?: string) => { + const flatten = (list: Comment[], parentId?: string) => { for (const comment of list) { const { replies, ...rest } = comment; result.push({ ...rest, parentId, isReply: Boolean(parentId) });