Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ body:
id: considerations
attributes:
label: "⚠️ 고려사항"
description: "성능, 접근성, 보안, 기존 사용자 영향 등 신경 쓸 점이 있으면 적어 주세요. 없으면 \"없음\"으로 두셔도 됩니다."
description: '성능, 접근성, 보안, 기존 사용자 영향 등 신경 쓸 점이 있으면 적어 주세요. 없으면 "없음"으로 두셔도 됩니다.'
validations:
required: false

- type: textarea
id: done
attributes:
label: "✅ 완료 조건"
description: "이 이슈를 닫아도 될 만큼 \"무엇이 되면 끝인지\" 적어 주세요."
description: '이 이슈를 닫아도 될 만큼 "무엇이 되면 끝인지" 적어 주세요.'
placeholder: "예) OO 시나리오에서 XX가 동작하고, 관련 테스트가 통과함."
validations:
required: true
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# frontend

한국외국어대학교 융복합소프트웨어 종합설계 프로젝트 프론트엔드 Repository
2 changes: 1 addition & 1 deletion docs/naming-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
- 예: `use-document-visible.ts`, `auth-api.ts`
- 상수: `SCREAMING_SNAKE_CASE`
- 변수 / 함수: `camelCase`
- 컴포넌트 / 타입 / 인터페이스 이름: `PascalCase`
- 컴포넌트 / 타입 / 인터페이스 이름: `PascalCase`
2 changes: 1 addition & 1 deletion public/assets/site.webmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
}
4 changes: 1 addition & 3 deletions src/app/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ export function RootLayout() {

return (
<div className="bg-background text-foreground">
<div
className="mx-auto flex h-dvh max-h-dvh min-h-0 w-full max-w-lg flex-col overflow-hidden px-page pt-[max(0.75rem,env(safe-area-inset-top))] pb-[max(0.75rem,env(safe-area-inset-bottom))] md:max-w-3xl xl:max-w-lg"
>
<div className="px-page mx-auto flex h-dvh max-h-dvh min-h-0 w-full max-w-lg flex-col overflow-hidden pt-[max(0.75rem,env(safe-area-inset-top))] pb-[max(0.75rem,env(safe-area-inset-bottom))] md:max-w-3xl xl:max-w-lg">
<Outlet />
</div>
</div>
Expand Down
20 changes: 12 additions & 8 deletions src/app/router/OnboardingGate.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ReactNode } from "react";
import type { ReactNode } from "react";
import { Navigate } from "react-router-dom";

import { useAuthStore } from "@/store/auth-store";
Expand All @@ -8,17 +8,21 @@ type OnboardingGateProps = {
};

/**
* 온보딩은 미로그인(또는 온보딩 미완료) 게스트 플로우 전용.
* 이미 온보딩까지 끝난 로그인 사용자는 홈으로 보냅니다.
* 온보딩 라우트 접근 제어
* - 비로그인: /login
* - 온보딩 완료: /room
* - 온보딩 미완료 로그인 유저만 진입 허용
*/
export function OnboardingGate({ children }: OnboardingGateProps) {
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);
const hasCompletedOnboarding = useAuthStore(
(s) => s.hasCompletedOnboarding,
);
const hasCompletedOnboarding = useAuthStore((s) => s.hasCompletedOnboarding);

if (isLoggedIn && hasCompletedOnboarding) {
return <Navigate to="/" replace />;
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}

if (hasCompletedOnboarding) {
return <Navigate to="/room" replace />;
}

return children;
Expand Down
14 changes: 7 additions & 7 deletions src/app/router/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import type { ReactNode } from "react";
import { Navigate } from "react-router-dom";
import type { ReactNode } from "react";
import { Navigate, Outlet } from "react-router-dom";

import { useAuthStore } from "@/store/auth-store";

type ProtectedRouteProps = {
children: ReactNode;
children?: ReactNode;
};

/**
* 로그인이 필요한 하위 라우트용 (예: `/settings` 추가 시).
* 현재 홈은 `RootIndexPage`에서 분기합니다.
* 인증이 필요한 하위 라우트를 보호.
* children이 없으면 <Outlet />을 렌더링해 레이아웃 라우트에서도 사용 가능.
*/
export function ProtectedRoute({ children }: ProtectedRouteProps) {
const isLoggedIn = useAuthStore((s) => s.isLoggedIn);

if (!isLoggedIn) {
return <Navigate to="/" replace />;
return <Navigate to="/login" replace />;
}

return children;
return children ?? <Outlet />;
}
32 changes: 23 additions & 9 deletions src/app/router/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { createBrowserRouter, Navigate } from "react-router-dom";
import { createBrowserRouter, Navigate, Outlet } from "react-router-dom";

import { RootLayout } from "@/app/layouts/RootLayout";
import { OnboardingGate } from "@/app/router/OnboardingGate";
import { ProtectedRoute } from "@/app/router/ProtectedRoute";
import { AuthCallbackPage } from "@/pages/AuthCallbackPage";
import { EntryPage } from "@/pages/EntryPage";
import { LoginPage } from "@/pages/LoginPage";
import { MapHomePage } from "@/pages/map/MapHomePage";
import { NicknamePage } from "@/pages/onboarding/NicknamePage";
import { TermsAgreementPage } from "@/pages/onboarding/TermsAgreementPage";
import { RoomMainPage } from "@/pages/room/RoomMainPage";
import { RootIndexPage } from "@/pages/RootIndexPage";
import { SplashScreenPage } from "@/pages/SplashScreenPage";
import { CoursePlannerPage } from "@/pages/tabs/CoursePlannerPage";
import { MyPage } from "@/pages/tabs/MyPage";
import { PlaceListPage } from "@/pages/tabs/PlaceListPage";
Expand All @@ -17,13 +20,10 @@ export const router = createBrowserRouter([
path: "/",
element: <RootLayout />,
children: [
{ index: true, element: <RootIndexPage /> },
{ path: "room", element: <RoomMainPage /> },
{ path: "map", element: <MapHomePage /> },
{ path: "list", element: <PlaceListPage /> },
{ path: "course", element: <CoursePlannerPage /> },
{ path: "mypage", element: <MyPage /> },
{ path: "login", element: <Navigate to="/" replace /> },
{ index: true, element: <EntryPage /> },
{ path: "dev/splash", element: <SplashScreenPage /> },
{ path: "login", element: <LoginPage /> },
{ path: "app", element: <Navigate to="/" replace /> },
{
path: "auth/callback",
element: <AuthCallbackPage />,
Expand All @@ -44,6 +44,20 @@ export const router = createBrowserRouter([
</OnboardingGate>
),
},
{
element: (
<ProtectedRoute>
<Outlet />
</ProtectedRoute>
),
children: [
{ path: "room", element: <RoomMainPage /> },
{ path: "map", element: <MapHomePage /> },
{ path: "list", element: <PlaceListPage /> },
{ path: "course", element: <CoursePlannerPage /> },
{ path: "mypage", element: <MyPage /> },
],
},
],
},
]);
2 changes: 1 addition & 1 deletion src/components/common/FloatingActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function FloatingActionButton({ label, className }: FloatingActionButtonP
className={cn(
"bg-brand-coral text-primary-foreground shadow-md",
"focus-visible:ring-ring flex size-12 items-center justify-center rounded-full outline-none",
"focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-offset-2",
"active:bg-brand-coral/90",
className,
)}
Expand Down
20 changes: 13 additions & 7 deletions src/components/room/FriendRoomItemView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export const FriendRoomItemView = memo(function FriendRoomItemView({
role="button"
tabIndex={0}
className={cn(
"transition-interaction-row grid origin-center grid-cols-[auto_minmax(0,1fr)_auto] grid-rows-[auto_auto] items-center gap-x-3.5 gap-y-px rounded-xl px-page py-3",
"cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"hover:bg-muted/30 active:scale-[0.995] active:bg-muted/40",
"transition-interaction-row px-page grid origin-center grid-cols-[auto_minmax(0,1fr)_auto] grid-rows-[auto_auto] items-center gap-x-3.5 gap-y-px rounded-xl py-3",
"focus-visible:ring-ring focus-visible:ring-offset-background cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"hover:bg-muted/30 active:bg-muted/40 active:scale-[0.995]",
)}
onClick={onOuterClick}
onContextMenu={onContextMenu}
Expand All @@ -54,13 +54,17 @@ export const FriendRoomItemView = memo(function FriendRoomItemView({
<User className="size-5" strokeWidth={2} />
</span>

<div className="text-room-title col-start-2 row-start-1 min-w-0 text-foreground">
<div className="text-room-title text-foreground col-start-2 row-start-1 min-w-0">
<span className="inline-flex max-w-full min-w-0 items-center gap-1">
<span className="min-w-0 truncate">{row.displayName}</span>
{pinned ? (
<span className="inline-flex shrink-0 text-muted-foreground" title="상단 고정됨" aria-label="상단 고정됨">
<span
className="text-muted-foreground inline-flex shrink-0"
title="상단 고정됨"
aria-label="상단 고정됨"
>
<Pin
className="size-3.5 fill-muted-foreground stroke-muted-foreground"
className="fill-muted-foreground stroke-muted-foreground size-3.5"
strokeWidth={2}
aria-hidden
/>
Expand All @@ -73,7 +77,9 @@ export const FriendRoomItemView = memo(function FriendRoomItemView({
</p>

<div className="col-start-3 row-span-2 row-start-1 flex shrink-0 flex-col items-center justify-center self-center">
<span className="text-room-meta cursor-pointer text-center text-brand-coral">{row.placeCount}개 장소</span>
<span className="text-room-meta text-brand-coral cursor-pointer text-center">
{row.placeCount}개 장소
</span>
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/room/FriendRoomList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const FriendRoomList = memo(function FriendRoomList({
);

return (
<ul className="divide-y divide-border/35" role="list">
<ul className="divide-border/35 divide-y" role="list">
{rows.map((row) => (
<li key={row.id}>
<FriendRoomItem row={row} onNavigate={navigateById} onOpenActionMenu={openMenuById} />
Expand Down
14 changes: 8 additions & 6 deletions src/components/room/InviteCodeModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,24 @@ const InviteCodeModalInner = memo(function InviteCodeModalInner({

return (
<RoomModalShell visible={visible} onOverlayClick={onClose} className="z-60">
<div className="px-7 pb-4 pt-7">
<h2 className="text-foreground text-base font-bold leading-tight">{displayRoom.displayName}</h2>
<div className="px-7 pt-7 pb-4">
<h2 className="text-foreground text-base leading-tight font-bold">
{displayRoom.displayName}
</h2>
</div>

<div className="px-6 pb-4 pt-2">
<div className="px-6 pt-2 pb-4">
<div
className={cn(
"flex flex-col items-center gap-3 rounded-xl border border-border/60 bg-muted/15 px-4 py-8",
"border-border/60 bg-muted/15 flex flex-col items-center gap-3 rounded-xl border px-4 py-8",
)}
>
<p className="text-center text-2xl font-semibold tabular-nums tracking-[0.35em] text-foreground">
<p className="text-foreground text-center text-2xl font-semibold tracking-[0.35em] tabular-nums">
{display}
</p>
<button
type="button"
className="rounded-full bg-muted px-4 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-muted/80"
className="bg-muted text-foreground hover:bg-muted/80 rounded-full px-4 py-1.5 text-xs font-medium transition-colors"
onClick={handleCopy}
>
복사
Expand Down
20 changes: 11 additions & 9 deletions src/components/room/LeaveRoomConfirmModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,25 @@ const LeaveRoomConfirmModalInner = memo(function LeaveRoomConfirmModalInner({

return (
<RoomModalShell visible={visible} onOverlayClick={onClose} className="z-60">
<div className="px-6 pb-5 pt-8 text-center">
<h2 className="text-foreground text-lg font-bold leading-snug">
방 나가기
</h2>
<p className="mt-4 text-sm leading-relaxed text-foreground">{LEAVE_WARNING}</p>
<div className="px-6 pt-8 pb-5 text-center">
<h2 className="text-foreground text-lg leading-snug font-bold">방 나가기</h2>
<p className="text-foreground mt-4 text-sm leading-relaxed">{LEAVE_WARNING}</p>
</div>

<div className="flex border-t border-border/50">
<div className="border-border/50 flex border-t">
<button
type="button"
className={cn(
"flex-1 py-4 text-sm font-medium transition-colors",
"border-r border-border/50 text-brand-coral hover:bg-muted/25 active:bg-muted/35",
"border-border/50 text-brand-coral hover:bg-muted/25 active:bg-muted/35 border-r",
)}
onClick={onClose}
>
취소
</button>
<button
type="button"
className="flex-1 py-4 text-sm font-medium text-foreground transition-colors hover:bg-muted/25 active:bg-muted/35"
className="text-foreground hover:bg-muted/25 active:bg-muted/35 flex-1 py-4 text-sm font-medium transition-colors"
onClick={handleConfirm}
>
나가기
Expand All @@ -66,7 +64,11 @@ const LeaveRoomConfirmModalInner = memo(function LeaveRoomConfirmModalInner({
/**
* 방 나가기 확인. RoomModalShell·presence 훅 패턴은 InviteCodeModal과 동일.
*/
export function LeaveRoomConfirmModal({ room, onClose, onConfirmLeave }: LeaveRoomConfirmModalProps) {
export function LeaveRoomConfirmModal({
room,
onClose,
onConfirmLeave,
}: LeaveRoomConfirmModalProps) {
const { displayRoom, visible } = useRoomActionModalPresence(room);

useEscapeKey(onClose, displayRoom != null);
Expand Down
8 changes: 5 additions & 3 deletions src/components/room/RoomActionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ const RoomActionModalPanel = memo(function RoomActionModalPanel({

return (
<RoomModalShell visible={visible} onOverlayClick={onClose}>
<div className="px-7 pb-2 pt-7">
<h2 className="text-foreground text-base font-bold leading-tight">{displayRoom.displayName}</h2>
<div className="px-7 pt-7 pb-2">
<h2 className="text-foreground text-base leading-tight font-bold">
{displayRoom.displayName}
</h2>
</div>

<div className="flex flex-col px-2 pb-3 pt-1">
<div className="flex flex-col px-2 pt-1 pb-3">
{actions.map(({ type, label, danger }) => (
<button
key={type}
Expand Down
4 changes: 2 additions & 2 deletions src/components/room/RoomMainShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ export type RoomMainShellProps = {
*/
export function RoomMainShell({ header, children, bottomNav, fab, className }: RoomMainShellProps) {
return (
<div className={cn("relative flex min-h-0 flex-1 flex-col -m-page", className)}>
<div className={cn("-m-page relative flex min-h-0 flex-1 flex-col", className)}>
{header}

<div className="scrollbar-hide bg-background min-h-0 flex-1 overflow-y-auto pb-room-main-scroll">
<div className="scrollbar-hide bg-background pb-room-main-scroll min-h-0 flex-1 overflow-y-auto">
{children}
</div>

Expand Down
7 changes: 6 additions & 1 deletion src/components/room/RoomModalShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export type RoomModalShellProps = {
/**
* RoomActionModal 등과 동일한 오버레이 + 흰 패널 래퍼 (transition 포함).
*/
export function RoomModalShell({ visible, onOverlayClick, children, className }: RoomModalShellProps) {
export function RoomModalShell({
visible,
onOverlayClick,
children,
className,
}: RoomModalShellProps) {
return (
<div
className={cn("fixed inset-0 z-50 flex items-center justify-center p-6", className)}
Expand Down
Loading
Loading