์ต๊ทผ์ ์จํ(On-Fit) ํ๋ก์ ํธ์์ โํจ๊ป ์ด๋ํ ๋ฉค๋ฒ์๊ฒ ์นญ์ฐฌ ๋ฆฌ๋ทฐ ๋จ๊ธฐ๊ธฐโ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๊ฒ ๋๋ค.
๊ฐ๋ฐํ๋ฉด์ ์์ฐ์ค๋ฝ๊ฒ Next.js App Router, Supabase, API ์ค๊ณ, ๊ทธ๋ฆฌ๊ณ UX์ ์ธ ๊ณ ๋ฏผ๊น์ง ์ด์ด์ก๊ณ ,
๋ด๊ฐ ์ด๋ค ๊ณผ์ ์ ๊ฑฐ์ณ ๊ธฐ๋ฅ์ ์์ฑํ๋์ง ๊ธฐ๋กํด๋ณด๊ณ ์ ํ๋ค.
์ด๋ฒ์ ๊ตฌํํ ํต์ฌ ๊ธฐ๋ฅ์ ํฌ๊ฒ ๋ ๊ฐ์ง๋ค.
์ด ๋ ๊ฐ์ง๊ฐ ํฉ์ณ์ ธ์ผ ์ ์ฒด ๋ฆฌ๋ทฐ ํ๋ก์ฐ๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐ๋๋ค.
๋ฆฌ๋ทฐ๋ ๋ค๋ฅธ ์ฌ๋์๊ฒ๋ง ๋จ๊ธธ ์ ์๊ธฐ ๋๋ฌธ์,
API์์ ์ฐธ์ฌ์ ์ ๋ณด ๋ฐ์ ๋ ๋ด ์ ๋ณด๋ ์ ์ธํด์ผ ํ๋ค.
์๋ ์ฐธ์ฌ์ ์กฐํ๋ ์ด๋ ๊ฒ ๋์ด ์์๋ค.
const { data, error } = await supabase
.from("participants")
.select(`
user_id,
role,
joined_at,
profiles (
id,
nickname,
profile_image
)
`)
.eq("room_id", roomId);
๋ฌธ์ ๋ ๋๊น์ง ํฌํจ๋์ด ์๋ค๋ ๊ฒ.
์ฒ์์ ํ๋ก ํธ์์ ํํฐ๋งํ ๊น ํ์ง๋ง,
API ๋จ์์ ํํฐ๋งํ๋ ๊ฒ ๋ ๊น๋ํ๊ณ , ํ์ ์์๋ ๋ช ํํ๋ค.
๊ทธ๋์ API์ excludeSelf=true ์ต์
์ ์ฃผ๋ฉด ๋๋ฅผ ๋นผ๋๋ก ๊ฐ์ ํ๋ค.
const { ok: hasUser, user, response } = await requireUserOr401(supabase);
if (!hasUser || !user) return response;
const { data, error } = await supabase
.from("participants")
.select("user_id, role, profiles(nickname, profile_image)")
.eq("room_id", roomId)
.neq("user_id", user.id); // ๐ฏ ๋ ์ ์ธ
์ด์ ๋ฆฌ๋ทฐ ํ์ด์ง์์๋ ์ด๋ ๊ฒ ๋ถ๋ฌ์ค๊ธฐ๋ง ํ๋ฉด ๋๋ค.
api.get("/api/chat/participants", {
params: { roomId, excludeSelf: true },
});
์ด๊ฑธ ์ ์ฉํ๋ ๋ฆฌ๋ทฐ ํ์ด์ง์๋ ์ ๋ง "๋ค๋ฅธ ๋ฉค๋ฒ๋ค๋ง" ๋จ๊ฒ ๋์๊ณ ,
UX์ ์ผ๋ก๋ ํจ์ฌ ์์ฐ์ค๋ฌ์์ก๋ค.
๋ค์์ผ๋ก ๊ณ ๋ฏผํ ๊ฑด ์ธ์ ๋ฆฌ๋ทฐ๋ฅผ ํ ์ ์๊ฒ ํ ๊ฒ์ธ๊ฐ์๋ค.
์จํ์์๋ post์ date_time์ด ์ด๋ ์์ ์๊ฐ์ด์ ๋ชจ์ง ๋ง๊ฐ ์๊ฐ์ด๋ค.
์ฆ, ์กฐ๊ฑด์ ๊ฐ๋จํ๋ค.
posts.date_time < ํ์ฌ ์๊ฐ(now) ์ผ ๋๋ง ๋ฒํผ์ ๋ณด์ฌ์ฃผ์
๊ทธ๋ฌ๊ธฐ ์ํด /api/chat/rooms ์๋ต์
๊ฐ room์ ๋ชจ์ง ๋ง๊ฐ ์๊ฐ๊ณผ canReview ์ฌ๋ถ๋ฅผ ์ถ๊ฐํ๋ค.
const now = new Date();
const recruitEndAt = post?.date_time ?? null;
const canReview =
recruitEndAt !== null ? new Date(recruitEndAt) <= now : false;
rooms API์์ ๋ด๋ ค์ฃผ๋ ์ต์ข ๋ฐ์ดํฐ๋ ์ด๋ ๊ฒ ๋ณํ๋ค.
{
"roomId": "123",
"title": "๊ฐ๋จ ๋ฐฐ๋๋ฏผํด",
"currentParticipants": 4,
"lastMessage": "๋ด์ผ ๋ต์ด์!",
"recruitEndAt": "2025-01-21T18:00:00Z",
"canReview": true}
ํ๋ก ํธ์์๋ ๊ทธ๋ฅ ์ด ๊ฐ๋ง ๋ณด๊ณ ๋ ๋๋งํ๋ฉด ๋๋ค.
{canReview && (
<div className="mt-3 flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleLeaveClick}>
๋๊ฐ๊ธฐ
</Button>
<Button variant="default" size="sm" onClick={handleReviewClick}>
๋ฆฌ๋ทฐํ๋ฌ๊ฐ๊ธฐ
</Button>
</div>
)}
canReview ๊ฒฐ๊ณผ๊ฐ true์ผ ๋๋ง ๋ฒํผ์ด ๋ณด์ธ๋ค!
์ด์ ๋ชจ์ง์ด ๋๋๊ธฐ ์ ์๋
๋ชจ์ง์ด ๋๋ ํ์๋
์์ฃผ ์์ฐ์ค๋ฌ์ด UX ํ๋ฆ.
์ฒ์์๋ ๋๊ฐ๊ธฐ ํ router.refresh()๋ฅผ ์ฌ์ฉํ๋๋ฐ,
ChatRoomList๊ฐ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ(useEffect + axios)๋ผ์ ์๋ก๊ณ ์นจ์ฒ๋ผ ๋ณด์ด์ง ์์๋ค.
๊ทธ๋์ ์๋์ฒ๋ผ state์์ ํด๋น room์ ์ง์ ์ ๊ฑฐํ๋ ๋ฐฉ์์ผ๋ก ํด๊ฒฐํ๋ค.
onLeave={(roomId) => {
setRooms(prev => prev.filter(r => r.roomId !== roomId));
}}
๊ฒฐ๊ณผ์ ์ผ๋ก ์ฆ์ UI์์ ์ฌ๋ผ์ง๊ณ , ํจ์ฌ ์์ฐ์ค๋ฌ์ด UX๊ฐ ์์ฑ๋์๋ค.
์ด๋ฒ ๊ธฐ๋ฅ์ ๋จ์ํ UI ํ๋ ์ถ๊ฐํ๋ ๊ฒ์ฒ๋ผ ๋ณด์์ง๋ง
โ๋ฐ์ดํฐ ํ๋ฆ โ API ์ค๊ณ โ UI ์กฐ๊ฑด ๋ถ๊ธฐ โ UX ์์ฐ์ค๋ฌ์โ๊น์ง
์ ๋ฐ์ ์ธ ๋ก์ง์ ๋ค์ ์ ๋นํ๋ ๊ณ๊ธฐ๊ฐ ๋์๋ค.
ํนํ:
์ด ์ธ ๊ฐ์ง๊ฐ ์กฐํ๋กญ๊ฒ ๋์ํ๋๋ก ๋ง๋ค๋ฉด์
๋๋ฉ์ธ ์ฃผ๋์ ์ธ ์ค๊ณ์ ์ค์์ฑ์ ๋ค์ ์ฒด๊ฐํ ์ ์์๋ค.
์์ผ๋ก๋ โ๋ฆฌ๋ทฐ ์ ์ ์๊ฐํโ, โ๋ฆฌ๋ทฐ ์๋ฆผโ, โ๋ฆฌ๋ทฐ ํต๊ณโ ๊ฐ์ ๊ธฐ๋ฅ๋ ์ถ๊ฐํด๋ณผ ๊ณํ์ด๋ค.