Skip to content

Commit 6dd9baa

Browse files
authored
Merge branch 'main' into feat/64-team-creation-member
2 parents 598cdf5 + 0579038 commit 6dd9baa

15 files changed

Lines changed: 769 additions & 567 deletions

.claude/launch.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"version": "0.0.1",
3+
"configurations": [
4+
{
5+
"name": "codedock-dev",
6+
"runtimeExecutable": "npm",
7+
"runtimeArgs": ["run", "dev"],
8+
"port": 5173
9+
}
10+
]
11+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ Thumbs.db
3737

3838
# Git fetch metadata accidentally created in the workspace root
3939
/FETCH_HEAD
40+
41+
# 로컬 도구 설정
42+
.claude/

README.md

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,111 @@
1+
# CodeDock
12

2-
# 안씨네 머슴들 - 수정본
3+
> 코드가 안전하게 출항하는 곳, CodeDock
34
4-
This is a code bundle for 안씨네 머슴들 - 수정본. The original project is available at https://www.figma.com/design/UjIbte4ow5gEtST62Zn5gx/%EC%95%88%EC%94%A8%EB%84%A4-%EB%A8%B8%EC%8A%B4%EB%93%A4---%EC%88%98%EC%A0%95%EB%B3%B8.
5+
CodeDock은 PR 리뷰, 보안 점검, API 문서화, ERD 관리, 팀 채팅을 하나의 흐름으로 연결하는 AI 개발 워크플로우 플랫폼입니다.
6+
팀원이 코드 변경의 맥락을 빠르게 파악하고, 리뷰와 문서 작업을 같은 워크스페이스 안에서 이어갈 수 있도록 돕습니다.
57

6-
## Running the code
8+
<p>
9+
<img alt="React" src="https://img.shields.io/badge/React-18.3.1-61DAFB?style=flat-square&logo=react&logoColor=061015" />
10+
<img alt="Vite" src="https://img.shields.io/badge/Vite-6.3.5-646CFF?style=flat-square&logo=vite&logoColor=white" />
11+
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-TSX-3178C6?style=flat-square&logo=typescript&logoColor=white" />
12+
<img alt="Tailwind CSS" src="https://img.shields.io/badge/Tailwind_CSS-4.x-38BDF8?style=flat-square&logo=tailwindcss&logoColor=061015" />
13+
</p>
714

8-
Run `npm i` to install the dependencies.
15+
## 주요 기능
16+
17+
| 영역 | 설명 |
18+
| --- | --- |
19+
| 랜딩 / 인증 | CodeDock 브랜딩, 채팅형 로그인/회원가입, 테마/언어 전환 |
20+
| 대시보드 | 팀과 리포지토리 기준의 리뷰 현황, 위험 신호, 최근 활동 확인 |
21+
| 워크스페이스 | 채널 기반 협업, 팀 채팅, 리포지토리별 작업 공간 |
22+
| PR 리뷰 | PR 목록, AI 리뷰 요약, Diff 확인, 라인 코멘트와 스레드 |
23+
| API 명세 | Swagger UI 기반 API 명세 확인 |
24+
| ERD | Mermaid 기반 ERD 작성, 미리보기, 확대/축소, 다운로드 |
25+
| 문서 | PR 리뷰 요약, API 변경 명세, ERD 변경 기록 등 템플릿 기반 문서 작성 |
26+
| 프로필 / 설정 | 사용자 프로필, GitHub 연동 관리, 테마 설정 |
27+
28+
## 기술 스택
29+
30+
| 분류 | 사용 기술 |
31+
| --- | --- |
32+
| Core | React, TypeScript, Vite |
33+
| Routing | React Router |
34+
| UI | Tailwind CSS, Radix UI, lucide-react |
35+
| Animation | motion |
36+
| Diagram / Docs | Mermaid, Swagger UI |
37+
| Interaction | react-dnd, resizable panels |
38+
39+
## 시작하기
40+
41+
### 요구 환경
42+
43+
- Node.js 18 이상 권장
44+
- npm
45+
46+
### 설치
47+
48+
```bash
49+
npm install
50+
```
51+
52+
### 개발 서버 실행
53+
54+
```bash
55+
npm run dev
56+
```
57+
58+
또는:
59+
60+
```bash
61+
npm start
62+
```
63+
64+
기본 Vite 주소는 `http://localhost:5173` 입니다.
65+
66+
### 빌드
67+
68+
```bash
69+
npm run build
70+
```
71+
72+
## 주요 라우트
73+
74+
| Route | 화면 |
75+
| --- | --- |
76+
| `/` | 랜딩 페이지 |
77+
| `/login` | 로그인 |
78+
| `/signup` | 회원가입 |
79+
| `/workspace` | 대시보드 |
80+
| `/chat` | 워크스페이스 / 팀 채팅 |
81+
| `/prs` | PR 목록 |
82+
| `/pr/:id` | PR 리뷰 룸 |
83+
| `/api-spec` | API 명세 |
84+
| `/erd` | ERD |
85+
| `/docs` | 문서 |
86+
| `/profile` | 프로필 |
87+
| `/settings` | 설정 |
88+
89+
## 프로젝트 구조
90+
91+
```text
92+
src/
93+
app/
94+
components/ # 공통 UI, 레이아웃, 채팅/리뷰 패널
95+
contexts/ # 테마, 언어 컨텍스트
96+
pages/ # 라우트 단위 페이지
97+
i18n/ # 다국어 번역 데이터
98+
styles/ # 전역 스타일, 테마, 폰트
99+
```
100+
101+
## 협업 규칙
102+
103+
- 이슈 단위로 브랜치를 생성합니다.
104+
- 커밋 메시지는 `feat:`, `fix:`, `refactor:`, `docs:`, `chore:` 등의 prefix를 사용합니다.
105+
- PR은 최소 1명 이상의 리뷰 승인을 받은 뒤 병합합니다.
106+
- 로컬 설정 파일, 빌드 결과물, 로그 파일은 커밋하지 않습니다.
107+
108+
##
109+
110+
AIBE5 Final Project Team 1 Frontend
9111

10-
Run `npm run dev` to start the development server.
11-

src/app/components/ChannelPanel.tsx

Lines changed: 66 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Hash, MessageSquare, Send, Bookmark, Share2, MoreVertical, X, Paperclip, Smile, UserPlus, FileUp, Image as ImageIcon, Link2 } from "lucide-react";
22
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
33
import { createFileMessageAttachment, createLinkMessageAttachment, createLinkMessageAttachmentFromText, messageAttachmentGroups, messageAttachmentTypeLabels, type MessageAttachment, type MessageAttachmentType } from "./messageAttachments";
4-
import { TypingIndicator } from "./TypingIndicator";
54
import { EmojiPicker } from "./EmojiPicker";
65
import { MessageReactions, toggleMessageReaction, type MessageReaction } from "./MessageReactions";
76
import { MessageAttachmentCard } from "./MessageAttachmentCard";
7+
import { TypingIndicatorBar } from "./TypingIndicatorBar";
88

99
interface Thread {
1010
id: number;
@@ -91,6 +91,19 @@ function saveThreads(storageKey: string, threads: Thread[]) {
9191
}
9292
}
9393

94+
const currentUserDisplayName = "김재준";
95+
const currentUserAvatar = currentUserDisplayName.charAt(0);
96+
const selfUserNames = new Set(["나", "me", "you", "jean", "jeaju", currentUserDisplayName]);
97+
98+
function isSelfUser(user?: string) {
99+
return selfUserNames.has((user ?? "").trim().toLowerCase());
100+
}
101+
102+
function getDisplayUserName(user?: string) {
103+
const trimmed = (user ?? "").trim();
104+
return isSelfUser(trimmed) ? currentUserDisplayName : trimmed;
105+
}
106+
94107
export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCounts = {}, onOpenThread, onOpenInvite, onToggleReaction }: ChannelPanelProps) {
95108
const channelStorageId = channelId ?? repoId ?? "general";
96109
const channelStorageKey = `${CHANNEL_THREADS_KEY_PREFIX}:${channelStorageId}`;
@@ -145,11 +158,15 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
145158
const scrollContainer = scrollContainerRef.current;
146159
if (!scrollContainer) return;
147160

148-
scrollContainer.scrollTo({
149-
top: scrollContainer.scrollHeight,
150-
behavior: "smooth"
161+
const frameId = window.requestAnimationFrame(() => {
162+
scrollContainer.scrollTo({
163+
top: scrollContainer.scrollHeight,
164+
behavior: "smooth"
165+
});
151166
});
152-
}, [threads.length, responderTyping]);
167+
168+
return () => window.cancelAnimationFrame(frameId);
169+
}, [threads.length, responderTyping, messageText]);
153170

154171
const triggerResponderTyping = () => {
155172
if (responderTypingTimerRef.current) {
@@ -170,15 +187,12 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
170187
const canSendMessage = messageText.trim().length > 0 || selectedAttachments.length > 0;
171188
const composerTyping = messageText.trim().length > 0;
172189
const typingLabel = responderTyping
173-
? "CodeDock AI가 답변을 정리 중입니다"
190+
? composerTyping
191+
? `CodeDock AI, ${currentUserDisplayName} 입력 중입니다`
192+
: "CodeDock AI가 답변을 정리 중입니다"
174193
: composerTyping
175194
? "내가 입력 중입니다"
176195
: "";
177-
const typingNote = responderTyping
178-
? "채널 맥락을 확인하고 다음 메시지를 준비합니다."
179-
: composerTyping
180-
? "팀원에게 입력 중 상태로 표시됩니다."
181-
: "";
182196

183197
const handleAttachmentToggle = (attachment: MessageAttachment) => {
184198
setSelectedAttachments((prev) =>
@@ -253,8 +267,8 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
253267

254268
const nextThread: Thread = {
255269
id: Date.now(),
256-
user: '나',
257-
avatar: '나',
270+
user: currentUserDisplayName,
271+
avatar: currentUserAvatar,
258272
message: trimmedMessage || `${outgoingAttachments.length}개 항목을 공유합니다.`,
259273
time: '방금',
260274
replies: 0,
@@ -319,30 +333,51 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
319333
<div className="grid gap-4">
320334
{threads.map((thread) => {
321335
const displayedReplyCount = replyCounts[thread.id] ?? thread.replies;
336+
const isOwnThread = isSelfUser(thread.user);
322337

323338
return (
324339
<div
325340
key={thread.id}
326341
className="rounded-xl overflow-hidden relative group"
327342
style={{
328-
background: 'rgba(5, 11, 20, 0.6)',
329-
border: '1px solid rgba(32, 227, 255, 0.14)'
343+
width: '100%',
344+
background: isOwnThread ? 'rgba(32, 227, 255, 0.075)' : 'rgba(5, 11, 20, 0.54)',
345+
border: isOwnThread ? '1px solid rgba(32, 227, 255, 0.18)' : '1px solid rgba(32, 227, 255, 0.14)',
346+
borderRadius: '12px',
347+
boxShadow: 'none'
330348
}}
331349
onMouseEnter={() => setHoveredMessageId(thread.id)}
332350
onMouseLeave={() => setHoveredMessageId(null)}
333351
>
334352
<div className="w-full px-5 py-4">
335353
<div className="flex items-start gap-3">
336-
<span style={{ fontSize: '28px', lineHeight: 1 }}>{thread.avatar}</span>
354+
<span className="grid h-10 w-10 flex-shrink-0 place-items-center rounded-full" style={{
355+
background: isOwnThread ? 'rgba(32, 227, 255, 0.16)' : 'rgba(32, 227, 255, 0.12)',
356+
border: isOwnThread ? '1px solid rgba(32, 227, 255, 0.30)' : '1px solid rgba(32, 227, 255, 0.22)',
357+
color: 'var(--neon-cyan)',
358+
fontSize: thread.avatar.length > 2 ? '18px' : '13px',
359+
fontWeight: 950,
360+
lineHeight: 1
361+
}}>{isOwnThread ? currentUserAvatar : thread.avatar}</span>
337362
<div className="flex-1 min-w-0">
338363
<div className="flex items-center gap-2 mb-1">
339364
<span className="tracking-tight" style={{
340365
fontSize: '13px',
341366
fontWeight: 900,
342-
color: 'var(--matrix-green)'
367+
color: isOwnThread ? 'var(--neon-cyan)' : 'var(--matrix-green)'
343368
}}>
344-
{thread.user}
369+
{isOwnThread ? getDisplayUserName(thread.user) : thread.user}
345370
</span>
371+
{isOwnThread && (
372+
<span className="rounded px-1.5 py-0.5 tracking-tight" style={{
373+
background: 'rgba(32, 227, 255, 0.12)',
374+
color: 'var(--neon-cyan)',
375+
fontSize: '10px',
376+
fontWeight: 950
377+
}}>
378+
내 메시지
379+
</span>
380+
)}
346381
<span className="tracking-tight" style={{
347382
fontSize: '11px',
348383
fontWeight: 700,
@@ -484,13 +519,6 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
484519
</div>
485520
);
486521
})}
487-
{typingLabel && (
488-
<TypingIndicator
489-
label={typingLabel}
490-
note={typingNote}
491-
avatar={responderTyping ? "AI" : "나"}
492-
/>
493-
)}
494522
</div>
495523
</div>
496524

@@ -693,20 +721,22 @@ export function ChannelPanel({ channelId, repoId, repoName, reactions, replyCoun
693721
onChange={(event) => handleLocalFilesSelected(event, "image")}
694722
/>
695723

696-
<div className="flex items-center gap-2 px-4 py-3 rounded-xl" style={{
724+
<TypingIndicatorBar label={typingLabel} />
725+
726+
<div className="relative flex items-center gap-2 px-4 py-3 rounded-xl" style={{
697727
background: 'rgba(5, 11, 20, 0.6)',
698728
border: '1px solid rgba(32, 227, 255, 0.14)'
699729
}}>
700-
<input
701-
type="text"
702-
value={messageText}
703-
onChange={(event) => setMessageText(event.target.value)}
704-
onKeyDown={handleMessageKeyDown}
705-
placeholder={`#${channelLabel}에 메시지 보내기`}
706-
className="min-w-0 flex-1 bg-transparent border-0 outline-none tracking-tight"
707-
style={{ color: 'var(--white)', fontSize: '14px', fontWeight: 700 }}
708-
/>
709-
<div className="flex shrink-0 items-center gap-1">
730+
<input
731+
type="text"
732+
value={messageText}
733+
onChange={(event) => setMessageText(event.target.value)}
734+
onKeyDown={handleMessageKeyDown}
735+
placeholder={`#${channelLabel}에 메시지 보내기`}
736+
className="min-w-0 flex-1 bg-transparent border-0 outline-none tracking-tight"
737+
style={{ color: 'var(--white)', fontSize: '14px', fontWeight: 700 }}
738+
/>
739+
<div className="flex shrink-0 items-center gap-1">
710740
<button
711741
onClick={() => togglePanel('attachment')}
712742
className="w-9 h-9 rounded-lg border-0 flex items-center justify-center transition-all cursor-pointer"

0 commit comments

Comments
 (0)