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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# DevWiki

등록된 멤버가 기술 용어, 면접 Q&A, 상황 시뮬레이션을 함께 작성하고 토론하는 회원 전용 개발자 지식 베이스입니다. Next.js와 Supabase를 사용합니다.
등록된 멤버가 기술 용어, 면접 Q&A, 상황 시뮬레이션을 함께 작성하고 댓글로 의견을 남기는 회원 전용 개발자 지식 베이스입니다. Next.js와 Supabase를 사용합니다.

## Stack

Expand All @@ -14,8 +14,8 @@
- Member-only access, role-based editing, profile nicknames
- Per-member favorites
- Database-backed full-text search with body snippets
- Expanded, threaded document discussions
- Recent discussion activity on the home dashboard
- Expanded, threaded document comments
- Recent comment activity on the home dashboard
- Share metadata, generated app icons, and document link copying

## Local setup
Expand All @@ -32,7 +32,7 @@ Supabase 연결 전에는 데모 문서가 보입니다. 실제 저장을 사용

- `기술 용어`: 기술 개념, 실무 예시, 꼬리 질문
- `면접 Q&A`: 기술/인성 질문과 답변 Tip
- `상황 시뮬레이션`: 서술형 상황 질문과 토론
- `상황 시뮬레이션`: 서술형 상황 질문과 트레이드오프

Supabase가 연결된 환경에서는 로그인한 active member만 문서를 읽을 수 있습니다. `공개` 상태는 인터넷 공개가 아니라 전체 멤버 기본 목록에 노출된다는 뜻입니다.
홈은 통합 검색과 개인 즐겨찾기 현황을 보여주는 메인 화면이고, `/terms`, `/interviews`, `/scenarios`에서 섹션별 문서를 탐색합니다.
Expand Down
2 changes: 1 addition & 1 deletion docs/releases/v0.10.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ Release date: 2026-06-02

### Features

* 최근 토론 활동 추가 ([39f23a5](https://github.com/geekgoing/devwiki/commit/39f23a55b4178443a987e43fd805e0ab1490489b))
* 최근 댓글 활동 추가 ([39f23a5](https://github.com/geekgoing/devwiki/commit/39f23a55b4178443a987e43fd805e0ab1490489b))
3 changes: 2 additions & 1 deletion src/app/(protected)/documents/[slug]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export default async function EditDocumentPage({
<CardContent className="px-5 py-6">
<h1 className="text-xl font-semibold">editor 권한이 필요합니다</h1>
<p className="mt-2 text-sm leading-6">
viewer는 문서를 읽고 토론할 수 있지만 문서 수정은 할 수 없습니다.
viewer는 문서를 읽고 댓글을 남길 수 있지만 문서 수정은 할 수
없습니다.
</p>
</CardContent>
</Card>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(protected)/documents/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export default async function NewDocumentPage({
<CardContent className="px-5 py-6">
<h1 className="text-xl font-semibold">editor 권한이 필요합니다</h1>
<p className="mt-2 text-sm leading-6">
viewer는 문서를 읽고 토론할 수 있지만 새 문서 작성은 할 수
viewer는 문서를 읽고 댓글을 남길 수 있지만 새 문서 작성은 할 수
없습니다.
</p>
</CardContent>
Expand Down
12 changes: 6 additions & 6 deletions src/app/(protected)/help/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ const contentSections = [
},
{
title: "상황 시뮬레이션",
body: "서술형 상황 질문을 문제 이해, 해결 전략, 트레이드오프 중심으로 토론합니다.",
body: "서술형 상황 질문을 문제 이해, 해결 전략, 트레이드오프 중심으로 정리합니다.",
},
];

const roleRows = [
["owner", "멤버 관리, 문서 작성/수정/복원, 이미지 업로드, 토론"],
["editor", "문서 작성/수정/복원, 이미지 업로드, 토론"],
["viewer", "문서 읽기와 토론 댓글 작성"],
["owner", "멤버 관리, 문서 작성/수정/복원, 이미지 업로드, 댓글 작성"],
["editor", "문서 작성/수정/복원, 이미지 업로드, 댓글 작성"],
["viewer", "문서 읽기와 댓글 작성"],
];

export default async function HelpPage() {
Expand Down Expand Up @@ -130,12 +130,12 @@ export default async function HelpPage() {
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare size={18} className="text-primary" aria-hidden />
토론
댓글
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm leading-6 text-muted-foreground">
문서마다 토론 영역이 있습니다. 질문의 의도, 더 좋은 답변 흐름,
문서마다 댓글 영역이 있습니다. 질문의 의도, 더 좋은 답변 흐름,
실제 면접에서 받은 꼬리 질문을 댓글로 남기고, 정리된 내용은
editor 이상이 문서 본문에 반영합니다.
</p>
Expand Down
46 changes: 23 additions & 23 deletions src/app/(protected)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ import {
documentDetailPath,
} from "@/lib/content-routes";
import { getCurrentMember, getCurrentUser } from "@/lib/auth";
import { getDocuments, getRecentDiscussions } from "@/lib/documents";
import { getDocuments, getRecentCommentActivities } from "@/lib/documents";
import { formatDate } from "@/lib/format";
import { isSupabaseConfigured } from "@/lib/supabase/env";
import type {
DocumentContentType,
DocumentSummary,
RecentDiscussion,
RecentCommentActivity,
} from "@/types/devwiki";

const sectionCards = [
Expand Down Expand Up @@ -134,38 +134,38 @@ function SmallDocumentLink({ document }: { document: DocumentSummary }) {
);
}

function DiscussionDocumentLink({
discussion,
function RecentCommentActivityLink({
activity,
}: {
discussion: RecentDiscussion;
activity: RecentCommentActivity;
}) {
const href = `${documentDetailPath(discussion.document)}#discussion`;
const href = `${documentDetailPath(activity.document)}#comments`;

return (
<Link
href={href}
className="group block rounded-lg border bg-muted/35 px-3 py-2 transition hover:border-primary/25 hover:bg-accent/60"
>
<span className="line-clamp-1 text-sm font-medium transition group-hover:text-primary">
{discussion.document.title}
{activity.document.title}
</span>
<span className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">
{discussion.latestCommentBody}
{activity.latestCommentBody}
</span>
<span className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>{contentTypeLabels[discussion.document.contentType]}</span>
<span>{contentTypeLabels[activity.document.contentType]}</span>
<span className="inline-flex items-center gap-1">
<MessageSquare size={11} aria-hidden />
{discussion.totalCommentCount}개
{activity.totalCommentCount}개
</span>
{discussion.replyCount ? (
<span>답글 {discussion.replyCount}개</span>
{activity.replyCount ? (
<span>답글 {activity.replyCount}개</span>
) : null}
</span>
<span className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
<Clock3 size={11} aria-hidden />
{discussion.latestCommentAuthorLabel} ·{" "}
{formatDate(discussion.latestCommentAt)}
{activity.latestCommentAuthorLabel} ·{" "}
{formatDate(activity.latestCommentAt)}
</span>
</Link>
);
Expand All @@ -177,13 +177,13 @@ export default async function Home() {
const member = await getCurrentMember();

const canReadPrivate = !configured || Boolean(member);
const [allDocuments, recentDiscussions] = await Promise.all([
const [allDocuments, recentCommentActivities] = await Promise.all([
getDocuments({
status: "active",
canReadPrivate,
viewerId: user?.id,
}),
getRecentDiscussions({
getRecentCommentActivities({
canReadPrivate,
viewerId: user?.id,
}),
Expand Down Expand Up @@ -361,20 +361,20 @@ export default async function Home() {
className="text-primary"
aria-hidden
/>
최근 토론
최근 댓글
</CardTitle>
</CardHeader>
<CardContent className="grid gap-2">
{recentDiscussions.length ? (
recentDiscussions.map((discussion) => (
<DiscussionDocumentLink
key={discussion.document.id}
discussion={discussion}
{recentCommentActivities.length ? (
recentCommentActivities.map((activity) => (
<RecentCommentActivityLink
key={activity.document.id}
activity={activity}
/>
))
) : (
<p className="rounded-lg bg-muted px-3 py-2 text-sm leading-6 text-muted-foreground">
아직 최근 토론이 없습니다.
아직 최근 댓글이 없습니다.
</p>
)}
</CardContent>
Expand Down
10 changes: 5 additions & 5 deletions src/components/document-comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export function DocumentComments({
const [replyingToCommentId, setReplyingToCommentId] = useState<string | null>(
null,
);
const canDiscuss = Boolean(configured && currentUserId && memberRole);
const canComment = Boolean(configured && currentUserId && memberRole);
const canModerate = canEditContent(memberRole ? { role: memberRole } : null);
const commentStats = getDocumentCommentStats(comments);

Expand All @@ -133,7 +133,7 @@ export function DocumentComments({
const isEditing = editingCommentId === comment.id;
const isReplying = replyingToCommentId === comment.id;
const canManage = canModerate || comment.createdBy === currentUserId;
const canReply = canDiscuss && !isReply && !isEditing;
const canReply = canComment && !isReply && !isEditing;
const replyCount = comment.replies.length;

return (
Expand Down Expand Up @@ -343,7 +343,7 @@ export function DocumentComments({
}

return (
<Card id="discussion" className="scroll-mt-24">
<Card id="comments" className="scroll-mt-24">
<CardHeader className="border-b pb-4 sm:px-6">
<div>
<CardTitle className="flex items-center gap-2">
Expand All @@ -352,7 +352,7 @@ export function DocumentComments({
className="text-muted-foreground"
aria-hidden
/>
토론
댓글
</CardTitle>
<CardDescription className="mt-1">
댓글 {commentStats.topLevelCount}개
Expand All @@ -366,7 +366,7 @@ export function DocumentComments({
</CardAction>
</CardHeader>
<CardContent className="space-y-5 sm:px-6">
{canDiscuss ? (
{canComment ? (
<form
ref={commentFormRef}
action={submitComment}
Expand Down
2 changes: 1 addition & 1 deletion src/components/document-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ export async function DocumentDetailPage({
<div className="rounded-lg bg-muted px-3 py-2">
<span className="inline-flex items-center gap-1.5">
<MessageSquare size={14} aria-hidden />
토론
댓글
</span>
<strong className="mt-1 block font-medium text-foreground">
{commentStats.totalCount}개
Expand Down
2 changes: 1 addition & 1 deletion src/components/document-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ const scenarioStarterMarkdown = `# 상황

## 답변 예시

## 토론 포인트
## 댓글로 확인할 점
`;

const starterMarkdownByType: Record<DocumentContentType, string> = {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/comment-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe("comment utils", () => {
expect(countDocumentComments(comments)).toBe(4);
});

it("counts flat comment rows for discussion summaries", () => {
it("counts flat comment rows for comment activity summaries", () => {
expect(
getFlatDocumentCommentStats([
{ parentCommentId: null },
Expand Down
2 changes: 1 addition & 1 deletion src/lib/content-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const contentTypeLabels: Record<DocumentContentType, string> = {
export const contentTypeSummaries: Record<DocumentContentType, string> = {
term: "기술 개념, 실무 예시, 꼬리 질문을 빠르게 훑습니다.",
interview_qa: "면접에서 받은 질문과 답변 Tip을 Q&A 형태로 정리합니다.",
scenario: "서술형 상황 질문을 해결 흐름과 토론 중심으로 다룹니다.",
scenario: "서술형 상황 질문을 해결 흐름과 트레이드오프 중심으로 다룹니다.",
};

export const contentRoutes = {
Expand Down
Loading