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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
- Mermaid diagrams
- Supabase Storage image uploads
- Member-only access, role-based editing, profile nicknames
- Per-member favorites and completed-learning filters
- Per-member favorites
- Database-backed full-text search with body snippets
- Expanded, threaded document discussions
- Share metadata, generated app icons, and document link copying
Expand All @@ -34,8 +34,8 @@ Supabase 연결 전에는 데모 문서가 보입니다. 실제 저장을 사용
- `상황 시뮬레이션`: 서술형 상황 질문과 토론

Supabase가 연결된 환경에서는 로그인한 active member만 문서를 읽을 수 있습니다. `공개` 상태는 인터넷 공개가 아니라 전체 멤버 기본 목록에 노출된다는 뜻입니다.
홈은 통합 검색과 개인 학습 현황을 보여주는 메인 화면이고, `/terms`, `/interviews`, `/scenarios`에서 섹션별 문서를 탐색합니다.
각 멤버는 문서를 즐겨찾기하거나 `숙지함`으로 표시할 수 있고, 검색/섹션 화면에서 `즐겨찾기`, `숙지함`, `미숙지` 필터로 학습 상태를 나눠 볼 수 있습니다.
홈은 통합 검색과 개인 즐겨찾기 현황을 보여주는 메인 화면이고, `/terms`, `/interviews`, `/scenarios`에서 섹션별 문서를 탐색합니다.
각 멤버는 문서를 즐겨찾기할 수 있고, 검색/섹션 화면에서 즐겨찾기 문서만 필터링할 수 있습니다.

## Image uploads

Expand Down
5 changes: 2 additions & 3 deletions scripts/verify-devwiki-mvp-data.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,6 @@ ${imageMarkdown}
{
document_id: documentId,
user_id: memberSession.user.id,
is_completed: true,
is_favorite: true,
},
{ onConflict: "document_id,user_id" },
Expand All @@ -498,7 +497,7 @@ ${imageMarkdown}

const { data: stateRows, error: stateLookupError } = await memberSession.client
.from("document_member_states")
.select("is_favorite, is_completed")
.select("is_favorite")
.eq("document_id", documentId)
.eq("user_id", memberSession.user.id);

Expand All @@ -510,7 +509,7 @@ ${imageMarkdown}
);
}

if (!stateRows[0].is_favorite || !stateRows[0].is_completed) {
if (!stateRows[0].is_favorite) {
throw new Error("Member document state values were not persisted.");
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/(protected)/interviews/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ContentSectionPage } from "@/components/content-section-page";
type InterviewsPageProps = {
searchParams: Promise<{
category?: string;
learning?: string;
favorites?: string;
status?: string;
}>;
};
Expand Down
44 changes: 13 additions & 31 deletions src/app/(protected)/me/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
CheckCircle2,
KeyRound,
MessageSquare,
RefreshCw,
Expand Down Expand Up @@ -56,14 +55,13 @@ type RecentCommentRow = {
} | null;
};

type LearningDocumentRow = {
type FavoriteDocumentRow = {
document: {
content_type: string;
slug: string;
title: string;
} | null;
document_id: string;
is_completed: boolean;
is_favorite: boolean;
updated_at: string;
};
Expand All @@ -83,7 +81,7 @@ type RawRecentCommentRow = Omit<RecentCommentRow, "document"> & {
| null;
};

type RawLearningDocumentRow = Omit<LearningDocumentRow, "document"> & {
type RawFavoriteDocumentRow = Omit<FavoriteDocumentRow, "document"> & {
documents?:
| {
content_type: string;
Expand Down Expand Up @@ -147,28 +145,27 @@ async function getRecentComments(userId: string) {
}));
}

async function getLearningDocuments(userId: string) {
async function getFavoriteDocuments(userId: string) {
const supabase = await createClient();
const { data, error } = await supabase
.from("document_member_states")
.select(
"document_id, is_favorite, is_completed, updated_at, documents(slug, title, content_type)",
"document_id, is_favorite, updated_at, documents(slug, title, content_type)",
)
.eq("user_id", userId)
.or("is_favorite.eq.true,is_completed.eq.true")
.eq("is_favorite", true)
.order("updated_at", { ascending: false })
.limit(8);

if (error) {
throw new Error(error.message);
}

return ((data ?? []) as RawLearningDocumentRow[]).map((row) => ({
return ((data ?? []) as RawFavoriteDocumentRow[]).map((row) => ({
document: Array.isArray(row.documents)
? (row.documents[0] ?? null)
: (row.documents ?? null),
document_id: row.document_id,
is_completed: row.is_completed,
is_favorite: row.is_favorite,
updated_at: row.updated_at,
}));
Expand All @@ -180,12 +177,12 @@ export default async function MePage({ searchParams }: MePageProps) {
const user = await getCurrentUser();
const member = await getCurrentMember();

const [recentDocuments, recentComments, learningDocuments] =
const [recentDocuments, recentComments, favoriteDocuments] =
configured && user && member
? await Promise.all([
getRecentDocuments(user.id),
getRecentComments(user.id),
getLearningDocuments(user.id),
getFavoriteDocuments(user.id),
])
: [[], [], []];

Expand Down Expand Up @@ -301,26 +298,20 @@ export default async function MePage({ searchParams }: MePageProps) {
<section className="grid gap-4 lg:grid-cols-2">
<Card className="lg:col-span-2">
<CardHeader>
<CardTitle>내 학습 상태</CardTitle>
<CardTitle>즐겨찾기</CardTitle>
<CardAction className="flex flex-wrap gap-2">
<Button asChild variant="outline" size="sm">
<Link href="/search?learning=favorite">
<Link href="/search?favorites=1">
<Star size={13} aria-hidden />
즐겨찾기
</Link>
</Button>
<Button asChild variant="outline" size="sm">
<Link href="/search?learning=completed">
<CheckCircle2 size={13} aria-hidden />
숙지함
</Link>
</Button>
</CardAction>
</CardHeader>
<CardContent>
{learningDocuments.length ? (
{favoriteDocuments.length ? (
<ol className="grid gap-2 md:grid-cols-2">
{learningDocuments.map((item) => (
{favoriteDocuments.map((item) => (
<li key={item.document_id}>
<Link
href={item.document ? documentHref(item.document) : "/"}
Expand All @@ -343,23 +334,14 @@ export default async function MePage({ searchParams }: MePageProps) {
즐겨찾기
</Badge>
) : null}
{item.is_completed ? (
<Badge
variant="outline"
className="border-teal-200 bg-teal-50 text-teal-700"
>
<CheckCircle2 size={12} aria-hidden />
숙지함
</Badge>
) : null}
</span>
</Link>
</li>
))}
</ol>
) : (
<p className="text-sm leading-6 text-muted-foreground">
아직 즐겨찾기하거나 숙지 완료한 문서가 없습니다.
아직 즐겨찾기한 문서가 없습니다.
</p>
)}
</CardContent>
Expand Down
79 changes: 15 additions & 64 deletions src/app/(protected)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
ArrowRight,
BookOpen,
CheckCircle2,
MessageSquareText,
Route,
Search,
Expand All @@ -10,7 +9,6 @@ import {
import Link from "next/link";

import { DocumentListCard } from "@/components/document-list-card";
import { LearningRouteBoard } from "@/components/learning-route-board";
import { SetupNotice } from "@/components/setup-notice";
import { Button } from "@/components/ui/button";
import {
Expand Down Expand Up @@ -124,12 +122,6 @@ function SmallDocumentLink({ document }: { document: DocumentSummary }) {
즐겨찾기
</span>
) : null}
{document.isCompleted ? (
<span className="inline-flex items-center gap-1 text-teal-700">
<CheckCircle2 size={11} aria-hidden />
숙지함
</span>
) : null}
</span>
</Link>
);
Expand All @@ -149,9 +141,6 @@ export default async function Home() {
const favoriteDocuments = allDocuments
.filter((document) => document.isFavorite)
.slice(0, 4);
const completedDocuments = allDocuments
.filter((document) => document.isCompleted)
.slice(0, 4);
const recentDocuments = allDocuments.slice(0, 6);
const counts = getDocumentCounts(allDocuments);
const documentsByContentType = getDocumentsByContentType(allDocuments);
Expand Down Expand Up @@ -195,38 +184,24 @@ export default async function Home() {

<Card className="bg-primary text-primary-foreground">
<CardHeader>
<CardTitle>내 학습 현황</CardTitle>
<CardTitle>즐겨찾기</CardTitle>
<CardDescription className="text-primary-foreground/75">
즐겨찾기와 숙지함을 기준으로 다시 볼 문서를 고릅니다.
다시 볼 문서를 빠르게 모아둡니다.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-2">
<Link
href="/search?learning=favorite"
className="rounded-lg bg-primary-foreground/10 p-3 transition hover:bg-primary-foreground/15"
>
<span className="flex items-center gap-1.5 text-xs text-primary-foreground/75">
<Star size={13} aria-hidden />
즐겨찾기
</span>
<span className="mt-2 block text-2xl font-semibold">
{favoriteDocuments.length}
</span>
</Link>
<Link
href="/search?learning=completed"
className="rounded-lg bg-primary-foreground/10 p-3 transition hover:bg-primary-foreground/15"
>
<span className="flex items-center gap-1.5 text-xs text-primary-foreground/75">
<CheckCircle2 size={13} aria-hidden />
숙지함
</span>
<span className="mt-2 block text-2xl font-semibold">
{completedDocuments.length}
</span>
</Link>
</div>
<Link
href="/search?favorites=1"
className="block rounded-lg bg-primary-foreground/10 p-3 transition hover:bg-primary-foreground/15"
>
<span className="flex items-center gap-1.5 text-xs text-primary-foreground/75">
<Star size={13} aria-hidden />
저장한 문서
</span>
<span className="mt-2 block text-2xl font-semibold">
{favoriteDocuments.length}
</span>
</Link>
</CardContent>
</Card>
</section>
Expand Down Expand Up @@ -309,7 +284,7 @@ export default async function Home() {
<CardTitle>즐겨찾기</CardTitle>
<CardAction>
<Button asChild variant="ghost" size="xs">
<Link href="/search?learning=favorite">전체</Link>
<Link href="/search?favorites=1">전체</Link>
</Button>
</CardAction>
</CardHeader>
Expand All @@ -325,32 +300,8 @@ export default async function Home() {
)}
</CardContent>
</Card>

<Card size="sm">
<CardHeader>
<CardTitle>숙지함</CardTitle>
<CardAction>
<Button asChild variant="ghost" size="xs">
<Link href="/search?learning=completed">전체</Link>
</Button>
</CardAction>
</CardHeader>
<CardContent className="grid gap-2">
{completedDocuments.length ? (
completedDocuments.map((document) => (
<SmallDocumentLink key={document.id} document={document} />
))
) : (
<p className="rounded-lg bg-muted px-3 py-2 text-sm leading-6 text-muted-foreground">
아직 숙지 완료한 문서가 없습니다.
</p>
)}
</CardContent>
</Card>
</aside>
</section>

<LearningRouteBoard documents={allDocuments} />
</div>
</main>
);
Expand Down
2 changes: 1 addition & 1 deletion src/app/(protected)/scenarios/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContentSectionPage } from "@/components/content-section-page";

type ScenariosPageProps = {
searchParams: Promise<{
learning?: string;
favorites?: string;
status?: string;
}>;
};
Expand Down
8 changes: 4 additions & 4 deletions src/app/(protected)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { DocumentSearchClient } from "@/components/document-search-client";
import { SetupNotice } from "@/components/setup-notice";
import {
parseFavoritesFilter,
parseInterviewCategory,
parseLearningFilter,
parseStatusFilter,
} from "@/lib/content-routes";
import { getCurrentMember, getCurrentUser } from "@/lib/auth";
Expand All @@ -13,7 +13,7 @@ import { isSupabaseConfigured } from "@/lib/supabase/env";
type SearchPageProps = {
searchParams: Promise<{
category?: string;
learning?: string;
favorites?: string;
q?: string;
status?: string;
}>;
Expand All @@ -22,8 +22,8 @@ type SearchPageProps = {
export default async function SearchPage({ searchParams }: SearchPageProps) {
const params = await searchParams;
const initialFilters = {
favoritesOnly: parseFavoritesFilter(params.favorites),
interviewCategory: parseInterviewCategory(params.category),
learning: parseLearningFilter(params.learning),
query: params.q?.trim() ?? "",
status: parseStatusFilter(params.status),
};
Expand All @@ -33,8 +33,8 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
const canReadPrivate = !configured || Boolean(member);
const documents = hasActiveDocumentQuery(initialFilters)
? await getDocuments({
favoritesOnly: initialFilters.favoritesOnly,
interviewCategory: initialFilters.interviewCategory,
learning: initialFilters.learning,
query: initialFilters.query,
status: initialFilters.status,
canReadPrivate,
Expand Down
2 changes: 1 addition & 1 deletion src/app/(protected)/terms/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ContentSectionPage } from "@/components/content-section-page";

type TermsPageProps = {
searchParams: Promise<{
learning?: string;
favorites?: string;
status?: string;
}>;
};
Expand Down
Loading
Loading