Skip to content

Commit 17f3707

Browse files
authored
Group page 채팅방 입니다 (#44)
* feat: GroupChatItem 수정중 * feat: subgoal에 대한 todo fetch api의 pagination 기반으로의 변경으로 인한 수정 api gen에서의 fetch반환 타입 정의에 버그가 있어 수동으로 fetch제작. 메인 페이지의 GoalCard, TodoList와 상세 페이지의 ListCard에서 새로 만든 훅을 적용. 또한, 기존에 사용하던 api의 반환 타입 변경으로 인해 연관된 영역에서 수정함. * feat: 세부 목표 완료 처리 * feat: 메인페이지 무한스크롤 적용 및 기존 무한스크롤 쿼리 훅 offset처리 수정 기존에는 size만큼 offset이 증가하지 않았어서 수정함. * feat: api gen 및 쓰지 않는 api관련 빌드 에러 수정 찌르기 api가 없어졌고 그룹 api로 통합된 것으로 보임. * feat: package lock 수정(npm 재설치) * feat: .env 파일 삭제 production에서 사용되는 듯 해서. * feat: 배포용 프록시 설정 추가 * feat: 배포 환경 api 프록시 수정 * feat: 상세페이지 목표 제거 api 연결 * feat: GoalDurationBottomSheet 디자인 적용 스타일 적용 안되어있었음 * feat: 상세 페이지 관련 컴포넌트 pr전 스토리북 생성 * feat: 상세 페이지 컴포넌트 스토리 경로 수정 * feat: 목표 완료 시 완료 이미지 뜨도록 수정 이전에는 progress가 100%일 때 뜨도록 했었는데, 목표 완료시 progress가 0이 되는 이슈가 있기도 해서 더 확실한 것으로 수정했습니다. * fix: 배너 y overflow 처리 추가 * feat: 상세 페이지 바텀 시트 추가 바텀시트에서 활성화 관련 불필요 부분 수정 추가. * feat: 아이콘 색상 커스텀 관련 변경에 따른 관련 영역 수정 기존에 사용되던 DetailBody에서와, GroupRoomHeader에서 색상 다르게 주입하도록 수정. GroupRoomHeader에서는 뒤로가기 라우팅 기능 추가 * feat: 그룹 챗 관련 무한쿼리 사용 및 수정 photoURL등에 대해선 백엔드 api 대기중. * fix: DetailBody에서 목표 완료 이후 목표 완료시키는 버튼 보이지 않도록 수정 * fix: DetailBody 목표 완료 이후에 목표완료모달 역시 뜨지 않도록 수정 * fix: GroupChatItem 스타일 수정에 따른 빌드 에러 수정 * feat: GroupChatRoom 리액션 추가 및 구조 변경 * feat: GroupChat수정해 머지 추가 * feat: 작업하던 채팅방 페이지로 변경 및 헤더에 라우팅 추가 * feat: 새로운 메시지 fetch시 강제 하단으로 스크롤 이동 처리 * feat: GroupRoomHeader 스토리에서 라우팅 안하도록 외부에서 콜백으로 주입하는 방법으로 변경 * fix: 타입 관련 빌드 에러 수정 불필요 콘솔도 제거 * feat: GroupChatRoom에서 파일 이름 및 url처리 * feat: 에러 페이지 추가 * feat: GroupChatItem에서 url값이 invalid할 때 처리 추가 방어코드용으로 추가했습니다. * feat: GroupRoomHeader 뒤로가기 버튼에서 그룹 페이지로 이동하도록 수정 스토리북에서의 작동을 위해 외부에서 주입하도록 변경 * feat: 상세 페이지에서의 DetailBody 뒤로가기 버튼 처리 수정 상세 페이지로부터의 이동은 그냥 뒤로가기로, 나머지는 group페이지로 이동하도록 수정 group페이지로부터 채팅방 이동 시 무한 로딩 생겨서.
1 parent 555e13f commit 17f3707

File tree

13 files changed

+702
-126
lines changed

13 files changed

+702
-126
lines changed

api/generated/motimo/Api.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,10 @@ export interface TodoResultRs {
291291
content?: string;
292292
/** 투두 기록 파일 url */
293293
fileUrl?: string;
294+
/** 투두 기록 파일 이름 */
295+
fileName?: string;
296+
/** 투두 기록 파일 데이터 종류 */
297+
fileMimeType?: string;
294298
}
295299

296300
export interface TodoRs {
@@ -421,6 +425,12 @@ export interface GroupMemberRs {
421425
isActivePoke?: boolean;
422426
}
423427

428+
export type GoalTitleUpdatedContent = GroupMessageContent & {
429+
/** @format uuid */
430+
goalId?: string;
431+
goalTitle?: string;
432+
};
433+
424434
/** 그룹 메시지 응답 */
425435
export interface GroupChatRs {
426436
/** 메시지 목록 */
@@ -493,6 +503,12 @@ export interface GroupMessageItemRs {
493503
sendAt: string;
494504
}
495505

506+
export type MessageReactionContent = GroupMessageContent & {
507+
/** @format uuid */
508+
referenceMessageId?: string;
509+
reactionType?: MessageReactionContentReactionTypeEnum;
510+
};
511+
496512
export type TodoCompletedContent = GroupMessageContent & {
497513
/** @format uuid */
498514
todoId?: string;
@@ -508,6 +524,8 @@ export type TodoResultSubmittedContent = GroupMessageContent & {
508524
emotion?: TodoResultSubmittedContentEmotionEnum;
509525
content?: string;
510526
fileUrl?: string;
527+
fileName?: string;
528+
mimeType?: string;
511529
};
512530

513531
export interface JoinedGroupRs {
@@ -811,6 +829,8 @@ export enum GroupMessageContentTypeEnum {
811829
LEAVE = "LEAVE",
812830
TODO_COMPLETE = "TODO_COMPLETE",
813831
TODO_RESULT_SUBMIT = "TODO_RESULT_SUBMIT",
832+
GOAL_TITLE_UPDATE = "GOAL_TITLE_UPDATE",
833+
MESSAGE_REACTION = "MESSAGE_REACTION",
814834
}
815835

816836
/**
@@ -823,8 +843,10 @@ export enum GroupMessageContentTypeEnum {
823843
interface BaseGroupMessageContentRs {
824844
/** 메시지 내용 */
825845
content:
846+
| GoalTitleUpdatedContent
826847
| GroupJoinContent
827848
| GroupLeaveContent
849+
| MessageReactionContent
828850
| TodoCompletedContent
829851
| TodoResultSubmittedContent;
830852
}
@@ -833,6 +855,14 @@ type BaseGroupMessageContentRsTypeMapping<Key, Type> = {
833855
type: Key;
834856
} & Type;
835857

858+
export enum MessageReactionContentReactionTypeEnum {
859+
GOOD = "GOOD",
860+
COOL = "COOL",
861+
CHEER_UP = "CHEER_UP",
862+
BEST = "BEST",
863+
LIKE = "LIKE",
864+
}
865+
836866
export enum TodoResultSubmittedContentEmotionEnum {
837867
PROUD = "PROUD",
838868
REGRETFUL = "REGRETFUL",
@@ -1950,7 +1980,7 @@ export class Api<SecurityDataType extends unknown> {
19501980
* @summary 그룹 나가기 API
19511981
* @request DELETE:/v1/groups/{groupId}/members/me
19521982
* @secure
1953-
* @response `200` `void` OK
1983+
* @response `204` `void` No Content
19541984
*/
19551985
exitGroup: (groupId: string, params: RequestParams = {}) =>
19561986
this.http.request<void, any>({

app/details/[goal_id]/_components/DetailsBody/DetailBody.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const DetailBody = ({ goalId }: DetailBodyProps) => {
125125
type="button"
126126
className="w-6 h-6 relative overflow-hidden text-label-assistive"
127127
>
128-
<Link href={`/group/${groupId}`}>
128+
<Link href={`/group/${groupId}?from=details`}>
129129
<RightArrowSvg />
130130
</Link>
131131
</button>

app/error.tsx

Lines changed: 71 additions & 0 deletions
Large diffs are not rendered by default.

app/group/[id]/page.tsx

Lines changed: 27 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,34 @@
11
"use client";
22

3-
import { use } from "react";
4-
import { AppBar } from "@/components/shared/AppBar/AppBar";
5-
import { GroupChat, SystemMessage } from "@/components/group";
6-
import { UsersGroupIcon } from "@/components/icons";
7-
import { useSafeRouter } from "@/hooks/useSafeRouter";
83
import {
9-
useGroupChat,
10-
useGroupDetail,
11-
useJoinedGroups,
12-
useMyProfile,
13-
} from "@/api/hooks";
14-
import { GetGroupChatParamsDirectionEnum } from "@/api/generated/motimo/Api";
15-
interface GroupDetailPageProps {
16-
params: Promise<{
17-
id: string;
18-
}>;
19-
}
20-
21-
export default function GroupDetailPage({ params }: GroupDetailPageProps) {
22-
const { id } = use(params);
23-
const { data: { name: title } = {}, error } = useGroupDetail(id);
24-
25-
const { data: groupChat } = useGroupChat(
26-
id,
27-
"0",
28-
"0",
29-
GetGroupChatParamsDirectionEnum.AFTER,
30-
);
31-
const router = useSafeRouter();
32-
33-
const handleBackClick = () => {
34-
router.push("/group");
35-
};
36-
37-
const handleReactionClick = (messageId: string) => {
38-
console.log("Reaction clicked for message:", messageId);
39-
// TODO: Implement reaction functionality
40-
};
41-
42-
const handleMemberClick = () => {
43-
router.push(`/group/${id}/member`);
44-
};
45-
46-
const { data: { nickname } = {} } = useMyProfile();
4+
useParams,
5+
usePathname,
6+
useRouter,
7+
useSearchParams,
8+
} from "next/navigation";
9+
import GroupChatRoom from "@/components/group/groupRoom/GroupChatRoom/GroupChatRoom";
10+
import GroupRoomHeader from "@/components/group/groupRoom/GroupRoomHeader/GroupRoomHeader";
11+
import { useGroupDetail } from "@/api/hooks";
12+
13+
export default function GroupRoom() {
14+
const { id: groupId } = useParams<{ id: string }>();
15+
const { data } = useGroupDetail(groupId);
16+
const router = useRouter();
17+
const fromPath = useSearchParams().get("from");
4718

4819
return (
49-
<div className="flex flex-col h-screen bg-white">
50-
{/* Status Bar Placeholder - 무시 */}
51-
52-
{/* AppBar */}
53-
<div className="flex items-center justify-between px-3 py-2 h-14 bg-white">
54-
<button
55-
onClick={handleBackClick}
56-
className="flex items-center justify-center w-6 h-6"
57-
aria-label="뒤로 가기"
58-
>
59-
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
60-
<path
61-
d="M15 18L9 12L15 6"
62-
stroke="#33363D"
63-
strokeWidth="1.5"
64-
strokeLinecap="round"
65-
strokeLinejoin="round"
66-
/>
67-
</svg>
68-
</button>
69-
70-
<h1 className="flex-1 ml-5 font-SUIT_Variable font-bold text-xl leading-[1.2] tracking-[-0.01em] text-black truncate">
71-
{title}
72-
</h1>
73-
74-
<button
75-
onClick={handleMemberClick}
76-
className="flex items-center justify-center w-10 h-10 rounded-full"
77-
aria-label="그룹 멤버 보기"
78-
>
79-
<UsersGroupIcon width={24} height={24} color="#464C53" />
80-
</button>
81-
</div>
82-
83-
{/* Main Content */}
84-
<div className="flex-1 overflow-y-auto">
85-
{/* System Message */}
86-
<div className="px-4 py-4">
87-
<SystemMessage message={`${nickname}님이 그룹에 입장했습니다.`} />
88-
</div>
89-
90-
{/* Chat Messages */}
91-
<div className="px-4 pb-4">
92-
<GroupChat
93-
messages={groupChat?.messages ?? []}
94-
onReactionClick={handleReactionClick}
95-
/>
96-
</div>
97-
</div>
98-
99-
{/* Bottom Gesture Bar Placeholder - 무시 */}
100-
</div>
20+
<main className="flex flex-col gap-4 bg-background-alternative min-h-screen pb-4 relative">
21+
<GroupRoomHeader
22+
groupRoomName={data?.name ?? ""}
23+
onBackClick={() => {
24+
if (fromPath && fromPath === "details") {
25+
router.back();
26+
}
27+
router.push("/group");
28+
}}
29+
routeToMember={() => router.push(`/group/${groupId}/member`)}
30+
/>
31+
<GroupChatRoom groupId={groupId} />
32+
</main>
10133
);
10234
}

components/group/GroupChat.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
GroupMessageItemRs,
55
TodoResultSubmittedContent,
66
} from "@/api/generated/motimo/Api";
7+
import { useMyProfile } from "@/api/hooks";
78

89
interface GroupChatProps {
910
messages: GroupMessageItemRs[];
@@ -16,18 +17,21 @@ export const GroupChat = ({
1617
className,
1718
onReactionClick,
1819
}: GroupChatProps) => {
20+
const { data } = useMyProfile();
21+
1922
return (
2023
<div className={cn("flex flex-col gap-4 w-full", className)}>
2124
{messages.map((m) => (
2225
<GroupChatItem
2326
key={m.messageId}
2427
id={m.messageId}
25-
userId={m.userId}
28+
// userId={m.userId}
29+
type={m.userId === data?.id ? "me" : "member"}
2630
style={"todo"}
2731
hasUserReacted={m.hasUserReacted}
2832
reactionCount={m.reactionCount}
2933
userName={m.userName}
30-
mainText={m.message as TodoResultSubmittedContent}
34+
mainText={(m.message as TodoResultSubmittedContent).content ?? ""}
3135
checkboxLabel={"checkboxLabel"}
3236
isChecked={true}
3337
diaryText={"다이어리 텍스트"}

components/group/GroupChatItem.tsx

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@ import {
1717
} from "@/api/generated/motimo/Api";
1818

1919
interface GroupChatItemProps {
20-
style: "todo" | "photo" | "diary" | "reaction";
20+
type: "me" | "member";
21+
style: "todo" | "reaction";
2122
hasUserReacted?: boolean;
2223
reactionCount?: number;
2324
userName: string;
24-
userId: string;
25-
mainText: TodoResultSubmittedContent;
25+
// userId: string;
26+
mainText: string;
2627
checkboxLabel?: string;
2728
isChecked?: boolean;
2829
diaryText?: string;
30+
// fileUrl?: string;
31+
fileName?: string;
2932
photoUrl?: string;
3033
reactionType?: ReactionTypes;
3134
className?: string;
@@ -104,14 +107,17 @@ export const GroupChatItem = ({
104107
isChecked = true,
105108
diaryText,
106109
photoUrl,
110+
fileName,
107111
reactionType = "best",
108112
className,
109113
onReactionClick,
110114
id,
111-
userId: senderId,
115+
type,
116+
// userId: senderId,
112117
}: GroupChatItemProps) => {
113118
const { data: myProfile } = useMyProfile();
114-
const isMe = myProfile?.id === senderId;
119+
// const isMe = myProfile?.id === senderId;
120+
const isMe = type === "me";
115121

116122
return (
117123
<div
@@ -165,9 +171,10 @@ export const GroupChatItem = ({
165171
<div
166172
className={cn(
167173
"bg-[#F7F7F8] rounded-lg",
168-
style === "todo" && "py-3 px-4",
169-
style === "photo" && "p-3 w-[248px]",
170-
style === "diary" && "p-3 w-[248px]",
174+
style === "todo" &&
175+
`py-3 px-4 ${diaryText || photoUrl ? "w-[248px]" : ""}`,
176+
// style === "photo" && "p-3 w-[248px]",
177+
// style === "diary" && "p-3 w-[248px]",
171178
style === "reaction" && "py-3 px-4 w-[248px]",
172179
)}
173180
>
@@ -177,36 +184,53 @@ export const GroupChatItem = ({
177184
<div className="flex flex-col gap-1">
178185
{/* Main text */}
179186
<span className="font-SUIT_Variable font-bold text-sm leading-[1.4] tracking-[-0.01em] text-[#33363D]">
180-
{mainText.content}
187+
{mainText}
181188
</span>
182189

183190
{/* Checkbox for todo, photo, and diary styles */}
184-
{(style === "todo" || style === "photo" || style === "diary") &&
185-
checkboxLabel && (
186-
<ChatCheckbox checked={isChecked} label={checkboxLabel} />
187-
)}
191+
{/* {(style === "todo" || style === "photo" || style === "diary") && */}
192+
{style === "todo" && checkboxLabel && (
193+
<ChatCheckbox checked={isChecked} label={checkboxLabel} />
194+
)}
188195
</div>
189196

190197
{/* Photo for photo style */}
191-
{style === "photo" && photoUrl && (
198+
{/* {style === "photo" && photoUrl && ( */}
199+
{style === "todo" && photoUrl && (
192200
<div className="w-[116px] h-[116px] rounded-lg border border-[#CDD1D5] overflow-hidden">
193-
<Image
194-
src={photoUrl}
195-
alt="첨부 이미지"
196-
width={116}
197-
height={116}
198-
className="w-full h-full object-cover"
199-
/>
201+
{photoUrl.startsWith("http") ? (
202+
<Image
203+
src={photoUrl}
204+
alt="첨부 이미지"
205+
width={116}
206+
height={116}
207+
className="w-full h-full object-cover"
208+
/>
209+
) : (
210+
<p className="font-['SUIT_Variable']"> Invalid URL path</p>
211+
)}
200212
</div>
201213
)}
202214

203215
{/* Diary text for diary style */}
204-
{style === "diary" && diaryText && (
216+
{/* {style === "diary" && diaryText && ( */}
217+
{style === "todo" && diaryText && (
205218
<p className="font-SUIT_Variable font-medium text-sm leading-[1.4] tracking-[-0.01em] text-[#464C53]">
206219
{diaryText}
207220
</p>
208221
)}
209222

223+
{style === "todo" && fileName && (
224+
<div
225+
className="pl-4 pr-3 py-2 relative bg-background-assistive rounded-lg inline-flex flex-col justify-center items-start gap-2 overflow
226+
-hidden"
227+
>
228+
<p className="justify-start text-label-normal text-sm font-bold font-['SUIT_Variable'] leading-tight">
229+
{fileName}
230+
</p>
231+
</div>
232+
)}
233+
210234
{/* Reaction illustration for reaction style */}
211235
{style === "reaction" && (
212236
<ReactionIllustration type={reactionType} />

components/group/ReactionModal.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ const ReactionModal = ({ onLeaveReaction, onClose }: ReactionModalProps) => {
2727
<p className="justify-start text-label-strong text-base font-bold font-['SUIT_Variable'] leading-tight">
2828
리액션
2929
</p>
30-
<button className="w-6 h-6 relative overflow-hidden">
30+
<button
31+
onClick={() => onClose()}
32+
type="button"
33+
className="w-6 h-6 relative overflow-hidden"
34+
>
3135
<CloseSvg />
3236
</button>
3337
</div>

0 commit comments

Comments
 (0)