Skip to content

Commit d838140

Browse files
authored
Merge pull request #54 from junotb/refactor/2026-02-16
refactor: study/teach/admin 라우트 분리 및 공통 컴포넌트 정리
2 parents 3fef75b + 2424724 commit d838140

83 files changed

Lines changed: 1913 additions & 1012 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

web/src/app/(landing)/page.tsx

Lines changed: 14 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
"use client";
22

33
import AuthModalContainer from "@/components/auth/AuthModalContainer";
4+
import LandingServiceMockup from "@/components/landing/LandingServiceMockup";
45
import { useAuthModalStore } from "@/stores/useAuthModalStore";
56
import { Button } from "@/components/ui/button";
67
import { cn } from "@/lib/utils";
8+
import { LANDING_FEATURES } from "@/constants/landing";
79

10+
/**
11+
* 랜딩 페이지.
12+
* 서비스 소개, CTA, 인증 모달.
13+
*/
814
export default function LandingPage() {
915
const { openModal } = useAuthModalStore();
1016

@@ -31,14 +37,16 @@ export default function LandingPage() {
3137
</h1>
3238

3339
<p className="mt-8 text-lg font-medium text-pretty text-muted-foreground sm:text-xl/8">
34-
휘발되는 수업은 이제 그만. 1:1 대면 수업의 몰입감과 LLM이 분석한
35-
데이터 기반 피드백으로 당신의 언어를 완성하세요.
40+
수업이 끝나도 사라지지 않습니다.
41+
<br className="hidden sm:block" />
42+
Google Meet 1:1 화상 수업 후, 녹화본 업로드 시 자동 자막과 LLM
43+
교정 피드백으로 당신의 언어를 완성하세요.
3644
</p>
3745

3846
<div className="mt-12 flex flex-col items-center gap-y-4 sm:flex-row sm:gap-x-6 lg:justify-start">
3947
<Button
40-
onClick={() => openModal("signin")}
41-
aria-label="로그인 페이지로 이동"
48+
onClick={() => openModal("signup")}
49+
aria-label="무료 회원가입으로 시작하기"
4250
className={cn(
4351
"rounded-2xl px-8 py-4 text-lg font-bold shadow-xl shadow-primary/20 transition-transform hover:-translate-y-1"
4452
)}
@@ -47,11 +55,7 @@ export default function LandingPage() {
4755
</Button>
4856
</div>
4957
<dl className="mt-16 grid grid-cols-2 gap-8 border-t border-border pt-8 sm:grid-cols-3">
50-
{[
51-
["WebRTC 1:1", "초저지연 대면 수업"],
52-
["STT Analysis", "모든 대화의 기록"],
53-
["LLM Feedback", "AI 정밀 교정 보고서"],
54-
].map(([title, desc]) => (
58+
{LANDING_FEATURES.map(([title, desc]) => (
5559
<div key={title} className="flex flex-col gap-y-1">
5660
<dt className="text-sm font-bold text-foreground uppercase tracking-wider">
5761
{title}
@@ -62,79 +66,7 @@ export default function LandingPage() {
6266
</dl>
6367
</div>
6468

65-
<div className="relative lg:ml-auto">
66-
<div className="relative z-10 w-full overflow-hidden rounded-2xl border border-border/50 bg-background shadow-2xl shadow-foreground/5 lg:w-[520px]">
67-
<div className="flex items-center gap-2 border-b border-border/50 bg-muted/30 px-4 py-2.5">
68-
<div className="flex gap-1.5">
69-
<span className="h-2.5 w-2.5 rounded-full bg-zinc-400" />
70-
<span className="h-2.5 w-2.5 rounded-full bg-zinc-400" />
71-
<span className="h-2.5 w-2.5 rounded-full bg-zinc-400" />
72-
</div>
73-
<span className="ml-2 truncate text-[11px] text-muted-foreground">
74-
classroom.nexlang.io
75-
</span>
76-
</div>
77-
78-
<div className="grid gap-3 p-3 sm:grid-cols-[1fr_200px]">
79-
<div className="relative overflow-hidden rounded-lg bg-zinc-950 aspect-[4/3] flex items-center justify-center">
80-
<div className="absolute inset-0 bg-[radial-gradient(ellipse_80%_60%_at_50%_50%,var(--color-primary)/8,transparent)]" />
81-
<div className="relative text-center">
82-
<div className="mx-auto mb-3 flex h-14 w-14 items-center justify-center rounded-full bg-zinc-800/90 ring-1 ring-zinc-700/50">
83-
<div className="h-6 w-6 rounded bg-primary/30" />
84-
</div>
85-
<p className="text-xs font-medium text-zinc-500">
86-
Google Meet 연결됨
87-
</p>
88-
<p className="mt-1 text-[10px] text-zinc-600">
89-
1:1 대면 수업 진행 중
90-
</p>
91-
</div>
92-
<div className="absolute right-2 bottom-2 h-16 w-20 overflow-hidden rounded-md bg-zinc-800/95 ring-1 ring-zinc-700/60">
93-
<div className="flex h-full w-full items-center justify-center">
94-
<span className="text-[10px] text-zinc-600">강사</span>
95-
</div>
96-
</div>
97-
</div>
98-
99-
<div className="rounded-lg border border-primary/20 bg-gradient-to-br from-primary/5 to-transparent p-4">
100-
<div className="mb-2.5 flex items-center gap-1.5">
101-
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />
102-
<span className="text-[10px] font-bold uppercase tracking-widest text-primary">
103-
AI 교정 피드백
104-
</span>
105-
</div>
106-
<p className="text-xs italic text-foreground/90">
107-
&quot;I have a plan to go there...&quot;
108-
</p>
109-
<div className="mt-2 rounded border-l-2 border-primary bg-muted/50 px-2 py-1.5">
110-
<p className="text-[11px] leading-relaxed text-muted-foreground">
111-
비즈니스 상황에서는{" "}
112-
<strong className="font-semibold text-foreground">
113-
&quot;I intend to visit...&quot;
114-
</strong>{" "}
115-
가 더 전문적입니다.
116-
</p>
117-
</div>
118-
</div>
119-
</div>
120-
121-
<div className="flex items-center justify-between border-t border-border/50 bg-zinc-900/80 px-4 py-2.5">
122-
<div className="flex items-center gap-2">
123-
<span className="text-[11px] text-zinc-500">남은 시간</span>
124-
<span className="font-mono text-sm font-medium tabular-nums text-zinc-300">
125-
00:45:22
126-
</span>
127-
</div>
128-
<div className="flex gap-2">
129-
<div className="h-7 w-16 rounded-md bg-zinc-700/60" />
130-
<div className="h-7 w-14 rounded-md border border-zinc-600/80 bg-zinc-800/60" />
131-
</div>
132-
</div>
133-
</div>
134-
135-
<div className="absolute -top-16 -right-16 -z-10 h-64 w-64 rounded-full bg-muted blur-3xl opacity-50" />
136-
<div className="absolute -bottom-16 -left-16 -z-10 h-64 w-64 rounded-full bg-muted blur-3xl opacity-50" />
137-
</div>
69+
<LandingServiceMockup />
13870
</div>
13971
</div>
14072
</section>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { redirect } from "next/navigation";
2+
import { auth } from "@/lib/auth";
3+
import { headers } from "next/headers";
4+
import SettingsLayoutContent from "@/components/settings/SettingsLayoutContent";
5+
6+
export default async function AdminSettingsLayout({
7+
children,
8+
}: {
9+
children: React.ReactNode;
10+
}) {
11+
const session = await auth.api.getSession({
12+
headers: await headers(),
13+
});
14+
15+
if (!session) {
16+
redirect("/");
17+
}
18+
19+
return (
20+
<SettingsLayoutContent
21+
dashboardPath="/admin"
22+
settingsBasePath="/admin/settings"
23+
>
24+
{children}
25+
</SettingsLayoutContent>
26+
);
27+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { redirect } from "next/navigation";
2+
3+
export default function AdminSettingsPage() {
4+
redirect("/admin/settings/profile");
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import PasswordForm from "@/components/settings/PasswordForm";
2+
3+
export default function AdminPasswordPage() {
4+
return <PasswordForm />;
5+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import ProfileForm from "@/components/settings/ProfileForm";
2+
3+
export default function AdminProfilePage() {
4+
return <ProfileForm />;
5+
}

web/src/app/auth/complete/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect } from "react";
44
import { useRouter } from "next/navigation";
55
import { authClient } from "@/lib/auth-client";
66
import { ROLE_REDIRECT_MAP } from "@/constants/auth";
7+
import type { SessionUser } from "@/types/auth";
78
import Loader from "@/components/common/Loader";
89

910
/**
@@ -16,8 +17,8 @@ export default function AuthCompletePage() {
1617
useEffect(() => {
1718
const redirect = async () => {
1819
const { data } = await authClient.getSession();
19-
const role = data?.user ? (data.user as { role?: string }).role : undefined;
20-
const path = role ? ROLE_REDIRECT_MAP[role] || "/" : "/";
20+
const user = data?.user as SessionUser | undefined;
21+
const path = user?.role ? ROLE_REDIRECT_MAP[user.role] : "/";
2122
router.replace(path);
2223
};
2324

Lines changed: 30 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,39 @@
1-
"use client";
2-
3-
import { useParams, useRouter } from "next/navigation";
4-
import ClassroomSkeleton from "@/components/classroom/ClassroomSkeleton";
5-
import MeetLinkArea from "@/components/classroom/MeetLinkArea";
6-
import LessonController from "@/components/classroom/LessonController";
7-
import { useLessonAccess } from "@/hooks/useLesson";
8-
import {
9-
Accordion,
10-
AccordionContent,
11-
AccordionItem,
12-
AccordionTrigger,
13-
} from "@/components/ui/accordion";
14-
15-
export default function ClassroomPage() {
16-
const params = useParams();
17-
const router = useRouter();
18-
const id = typeof params.id === "string" ? parseInt(params.id, 10) : null;
19-
20-
const { data, isLoading, isError } = useLessonAccess(id, { enabled: id != null && !Number.isNaN(id) });
1+
import { redirect } from "next/navigation";
2+
import { auth } from "@/lib/auth";
3+
import { headers } from "next/headers";
4+
import { getClassroomPath } from "@/lib/routes";
5+
import { USER_ROLE } from "@/constants/auth";
6+
import type { UserRole } from "@/schemas/user/user-role";
7+
8+
interface LegacyClassroomPageProps {
9+
params: Promise<{ id: string }>;
10+
}
2111

22-
if (id == null || Number.isNaN(id)) {
23-
return (
24-
<div className="flex-1 flex items-center justify-center text-zinc-500">
25-
잘못된 수업 번호입니다.
26-
</div>
27-
);
12+
/**
13+
* 기존 /classroom/[id] URL 호환. TEACHER|STUDENT만 역할별 경로로 리다이렉트.
14+
*/
15+
export default async function LegacyClassroomPage({
16+
params,
17+
}: LegacyClassroomPageProps) {
18+
const { id } = await params;
19+
const scheduleId = parseInt(id, 10);
20+
21+
if (Number.isNaN(scheduleId)) {
22+
redirect("/");
2823
}
2924

30-
if (isLoading) {
31-
return <ClassroomSkeleton />;
32-
}
25+
const session = await auth.api.getSession({
26+
headers: await headers(),
27+
});
3328

34-
if (isError || !data) {
35-
return (
36-
<div className="flex-1 flex items-center justify-center text-zinc-500">
37-
수업 정보를 불러올 수 없습니다.
38-
</div>
39-
);
29+
if (!session) {
30+
redirect("/");
4031
}
4132

42-
if (!data.allowed) {
43-
return null;
33+
const role = session.user.role as UserRole;
34+
if (role !== USER_ROLE.TEACHER && role !== USER_ROLE.STUDENT) {
35+
redirect("/");
4436
}
4537

46-
const { course } = data;
47-
48-
return (
49-
<div className="flex-1 flex flex-col min-h-0">
50-
<div className="flex-1 flex min-h-0 flex-col gap-2 p-2">
51-
<Accordion type="single" collapsible defaultValue="course" className="shrink-0 rounded-lg border border-zinc-800 bg-zinc-900/50">
52-
<AccordionItem value="course" className="border-zinc-800">
53-
<AccordionTrigger className="px-4 py-3 text-sm font-semibold text-zinc-200 hover:text-white hover:no-underline">
54-
{course.title}
55-
</AccordionTrigger>
56-
<AccordionContent className="px-4 text-zinc-400">
57-
{course.description ?? "설명이 없습니다."}
58-
</AccordionContent>
59-
</AccordionItem>
60-
</Accordion>
61-
<div className="flex-1 flex min-h-0">
62-
<MeetLinkArea meetLink={data.meetLink} />
63-
</div>
64-
</div>
65-
<LessonController
66-
scheduleId={id}
67-
role={data.role}
68-
startsAt={data.schedule.startsAt}
69-
endsAt={data.schedule.endsAt}
70-
onExit={() => router.back()}
71-
/>
72-
</div>
73-
);
38+
redirect(getClassroomPath(role, scheduleId));
7439
}

0 commit comments

Comments
 (0)