From eeef4099ce8f482a0786ed3dc1e89807276c0ed9 Mon Sep 17 00:00:00 2001 From: Shin-Yu-1 Date: Sun, 3 Aug 2025 10:07:34 +0900 Subject: [PATCH 01/30] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 174 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 107 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 7959ce42..69cba7eb 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,109 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - ...tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - ...tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - ...tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` +# 🌐 Web IDE + +React, TypeScript, Vite 기반의 μ‹€μ‹œκ°„ μ›Ή IDEμž…λ‹ˆλ‹€. +μ‹€μ‹œκ°„ μ½”λ“œ λ™μ‹œ νŽΈμ§‘, μ±„νŒ…, 파일 μ—…λ‘œλ“œ 및 λ‹€μš΄λ‘œλ“œ, GitHub 둜그인 κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. + +ν”„λ‘œμ νŠΈ κΈ°κ°„: 2025.07 ~ 2025.08 (기획 및 개발) +Link: https://www.deepdirect.site/ +Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com/DeepDirect/deepwebide-be) + +--- + +## 🫢 νŒ€μ› +| 이름 | μ—­ν•  | GitHub 링크 | +|----------|--------------------|------------------------------------------------| +| μ •μ›μš© | νŒ€μž₯, Full Stack | [@projectmiluju](https://github.com/jihun-dev) | +| κΆŒν˜œμ§„ | Backend | [@sunsetkk](https://github.com/sunsetkk) | +| 박건 | Frontend | [@Jammanb0](https://github.com/Jammanb0) | +| λ°•μ†Œν˜„ | Frontend | [@ssoogit](https://github.com/ssoogit) | +| λ°•μž¬κ²½ | Full Stack, Infra | [@Shin-Yu-1](https://github.com/sunghoon-back) | +| 이은지 | Frontend | [@ebbll](https://github.com/ebbll) | +| μ΅œλ²”κ·Ό | Backend | [@vayaconChoi](https://github.com/vayaconChoi) | + + +--- + +## πŸš€ μ£Όμš” κΈ°λŠ₯ +- μ‹€μ‹œκ°„ ν˜‘μ—… μ½”λ“œ νŽΈμ§‘ +- μ‹€μ‹œκ°„ μ±„νŒ… + +--- + +## πŸ› οΈ 기술 μŠ€νƒ -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default tseslint.config([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) +### FE +| λΆ„λ₯˜ | 기술λͺ… | +|----------------|----------------------------------------------------------------| +| ν”„λ ˆμž„μ›Œν¬/λŸ°νƒ€μž„ | React + TypeScript | +| λΉŒλ“œ 도ꡬ | Vite, TypeScript (tsc) | +| μƒνƒœ 관리 | Zustand (ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ), TanStack Query (μ„œλ²„ μƒνƒœ) | +| λΌμš°νŒ… | TanStack Router (@tanstack/react-router) | +| 폼 관리/검증 | React Hook Form, Zod, @hookform/resolvers | +| μ‹€μ‹œκ°„ | Yjs, y-monaco, y-websocket, WebSocket, SockJS | +| μ½”λ“œ 에디터 | Monaco Editor (@monaco-editor/react) | +| UI 라이브러리 | Radix UI (Toast, Tooltip, Switch, VisuallyHidden) | +| Drag & Drop | React DnD, react-dnd-html5-backend, @minoru/react-dnd-treeview | +| λ‚ μ§œ 처리 | dayjs | +| μ•„μ΄μ½˜ | pixelarticons, clsx | +| λ„€νŠΈμ›Œν¬ μš”μ²­ | Axios | +| μŠ€νƒ€μΌλ§ | SCSS | +| μ½”λ“œ ν’ˆμ§ˆ 도ꡬ | ESLint, Prettier, Stylelint, Husky, lint-staged | + +### BE +| λΆ„λ₯˜ | μ‚¬μš© 기술 / 라이브러리 | μ„€λͺ… | +|-----------------|------------------------------------------------------|------| +| μ–Έμ–΄ 및 ν”„λ ˆμž„μ›Œν¬ | Java 17, Spring Boot 3.4.7 | λ°±μ—”λ“œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 기반 | +| λΉŒλ“œ 도ꡬ | Gradle | μ˜μ‘΄μ„± 및 λΉŒλ“œ 관리 | +| μ›Ή μ„œλ²„ | Spring Web (`spring-boot-starter-web`) | REST API 및 MVC ꡬ성 | +| λ³΄μ•ˆ | Spring Security, JWT (`jjwt`), OAuth2 Client | 둜그인, 인증, 인가 처리 | +| λ°μ΄ν„°λ² μ΄μŠ€ | MySQL, Spring Data JPA (`hibernate`) | μ‚¬μš©μž 및 μ €μž₯μ†Œ κ΄€λ¦¬μš© RDB | +| μ‹€μ‹œκ°„ 톡신 | WebSocket (`spring-websocket`), STOMP, SockJS | μ±„νŒ…, λ™μ‹œ νŽΈμ§‘μš© μ‹€μ‹œκ°„ 톡신 | +| Redis | Lettuce (`lettuce-core`), Spring Data Redis | 토큰 μ €μž₯, Pub/Sub λ“± | +| 파일 μ—…λ‘œλ“œ | AWS S3 SDK (v1, v2 혼용) | μ½”λ“œ 파일 μ—…λ‘œλ“œ/λ‹€μš΄λ‘œλ“œ | +| 이메일 λ°œμ†‘ | Spring Mail (`spring-boot-starter-mail`) | 이메일 인증, μ•Œλ¦Ό 전솑 | +| 문자 인증 | Coolsms SDK (`net.nurigo:sdk`) | μ „ν™”λ²ˆν˜Έ 인증 (SMS) | +| API λ¬Έμ„œν™” | SpringDoc OpenAPI (`springdoc-openapi`) | Swagger 기반 μžλ™ API λ¬Έμ„œ | +| λͺ¨λ‹ˆν„°λ§ | Spring Boot Actuator | Health Check λ“± λ©”νŠΈλ¦­ 제곡 | +| μ—λŸ¬ 좔적 | Sentry (`sentry-spring-boot-starter`, logback 연동) | λŸ°νƒ€μž„ μ—λŸ¬ μ‹€μ‹œκ°„ 좔적 | +| λ‘œκΉ… μœ ν‹Έλ¦¬ν‹° | Logback, Commons IO | λ‘œκΉ…, 파일 μœ ν‹Έλ¦¬ν‹° | + +--- + +## πŸ“Έ Screenshots + + +--- + +## πŸ“ FE 디렉토리 ꡬ쑰 +```bash +src/ +β”œβ”€β”€ api/ # API μš”μ²­ ν•¨μˆ˜ μ •μ˜ (axios λ“±κ³Ό 연동) +β”œβ”€β”€ assets/ # 이미지 λ“± 정적 λ¦¬μ†ŒμŠ€ +β”œβ”€β”€ components/ # μž¬μ‚¬μš© κ°€λŠ₯ν•œ UI μ»΄ν¬λ„ŒνŠΈ +β”œβ”€β”€ constants/ # 곡톡 μƒμˆ˜ μ •μ˜ +β”œβ”€β”€ features/ # 도메인 λ‹¨μœ„μ˜ κΈ°λŠ₯ λͺ¨λ“ˆ +β”œβ”€β”€ hooks/ # μ»€μŠ€ν…€ React Hook λͺ¨μŒ +β”œβ”€β”€ layouts/ # νŽ˜μ΄μ§€ 곡톡 λ ˆμ΄μ•„μ›ƒ μ»΄ν¬λ„ŒνŠΈ +β”œβ”€β”€ mocks/ # Mock 데이터 (개발/ν…ŒμŠ€νŠΈ 용) +β”œβ”€β”€ pages/ # λΌμš°νŒ…λ˜λŠ” νŽ˜μ΄μ§€ μ»΄ν¬λ„ŒνŠΈ +β”œβ”€β”€ router/ # TanStack Router κ΄€λ ¨ λΌμš°ν„° μ„€μ • +β”œβ”€β”€ schemas/ # Zod 기반 μŠ€ν‚€λ§ˆ (μš”μ²­/응닡 νƒ€μž… μ •μ˜ 포함) +β”œβ”€β”€ stores/ # Zustand μƒνƒœ μ €μž₯μ†Œ +β”œβ”€β”€ styles/ # μ „μ—­ μŠ€νƒ€μΌ μ •μ˜ (reset, theme λ“±) +β”œβ”€β”€ types/ # μ „μ—­ νƒ€μž… μ •μ˜ (TypeScript interface/type λͺ¨μŒ) +└── utils/ # 곡톡 μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ ``` + +--- + +## πŸƒβ€βž‘οΈ μ„€μΉ˜ 및 μ‹€ν–‰ +```bash +# νŒ¨ν‚€μ§€ μ„€μΉ˜ +pnpm install + +# 개발 μ„œλ²„ μ‹€ν–‰ +pnpm dev + +# λΉŒλ“œ +pnpm build +``` \ No newline at end of file From cb03f3e9be9d158df2daef38d5e8eb05bf807e2c Mon Sep 17 00:00:00 2001 From: Shin-Yu-1 Date: Sun, 3 Aug 2025 10:11:41 +0900 Subject: [PATCH 02/30] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 69cba7eb..885da246 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@ React, TypeScript, Vite 기반의 μ‹€μ‹œκ°„ μ›Ή IDEμž…λ‹ˆλ‹€. μ‹€μ‹œκ°„ μ½”λ“œ λ™μ‹œ νŽΈμ§‘, μ±„νŒ…, 파일 μ—…λ‘œλ“œ 및 λ‹€μš΄λ‘œλ“œ, GitHub 둜그인 κΈ°λŠ₯을 μ œκ³΅ν•©λ‹ˆλ‹€. -ν”„λ‘œμ νŠΈ κΈ°κ°„: 2025.07 ~ 2025.08 (기획 및 개발) -Link: https://www.deepdirect.site/ +ν”„λ‘œμ νŠΈ κΈ°κ°„: 2025.07 ~ 2025.08 (기획 및 개발) +Link: https://www.deepdirect.site/ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com/DeepDirect/deepwebide-be) --- @@ -20,7 +20,6 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | 이은지 | Frontend | [@ebbll](https://github.com/ebbll) | | μ΅œλ²”κ·Ό | Backend | [@vayaconChoi](https://github.com/vayaconChoi) | - --- ## πŸš€ μ£Όμš” κΈ°λŠ₯ From 8b996accb239a43b49343bc70e65fe110f6aca49 Mon Sep 17 00:00:00 2001 From: Shin-Yu-1 Date: Sun, 3 Aug 2025 10:15:00 +0900 Subject: [PATCH 03/30] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 885da246..097f6d60 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | μƒνƒœ 관리 | Zustand (ν΄λΌμ΄μ–ΈνŠΈ μƒνƒœ), TanStack Query (μ„œλ²„ μƒνƒœ) | | λΌμš°νŒ… | TanStack Router (@tanstack/react-router) | | 폼 관리/검증 | React Hook Form, Zod, @hookform/resolvers | -| μ‹€μ‹œκ°„ | Yjs, y-monaco, y-websocket, WebSocket, SockJS | +| μ‹€μ‹œκ°„ | Yjs, y-monaco, y-websocket, STOMP, SockJS | | μ½”λ“œ 에디터 | Monaco Editor (@monaco-editor/react) | | UI 라이브러리 | Radix UI (Toast, Tooltip, Switch, VisuallyHidden) | | Drag & Drop | React DnD, react-dnd-html5-backend, @minoru/react-dnd-treeview | From 434bb5cf1a4fa8ab24fa38bc28e7895bd1b85284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9E=AC=EA=B2=BD?= <82097725+Shin-Yu-1@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:17:41 +0900 Subject: [PATCH 04/30] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 097f6d60..c8955027 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ React, TypeScript, Vite 기반의 μ‹€μ‹œκ°„ μ›Ή IDEμž…λ‹ˆλ‹€. Link: https://www.deepdirect.site/ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com/DeepDirect/deepwebide-be) +
+ --- ## 🫢 νŒ€μ› @@ -20,12 +22,16 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | 이은지 | Frontend | [@ebbll](https://github.com/ebbll) | | μ΅œλ²”κ·Ό | Backend | [@vayaconChoi](https://github.com/vayaconChoi) | +
+ --- ## πŸš€ μ£Όμš” κΈ°λŠ₯ - μ‹€μ‹œκ°„ ν˜‘μ—… μ½”λ“œ νŽΈμ§‘ - μ‹€μ‹œκ°„ μ±„νŒ… +
+ --- ## πŸ› οΈ 기술 μŠ€νƒ @@ -48,6 +54,8 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | μŠ€νƒ€μΌλ§ | SCSS | | μ½”λ“œ ν’ˆμ§ˆ 도ꡬ | ESLint, Prettier, Stylelint, Husky, lint-staged | +
+ ### BE | λΆ„λ₯˜ | μ‚¬μš© 기술 / 라이브러리 | μ„€λͺ… | |-----------------|------------------------------------------------------|------| @@ -66,11 +74,16 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | μ—λŸ¬ 좔적 | Sentry (`sentry-spring-boot-starter`, logback 연동) | λŸ°νƒ€μž„ μ—λŸ¬ μ‹€μ‹œκ°„ 좔적 | | λ‘œκΉ… μœ ν‹Έλ¦¬ν‹° | Logback, Commons IO | λ‘œκΉ…, 파일 μœ ν‹Έλ¦¬ν‹° | +
+ --- ## πŸ“Έ Screenshots + +
+ --- ## πŸ“ FE 디렉토리 ꡬ쑰 @@ -93,6 +106,8 @@ src/ └── utils/ # 곡톡 μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜ ``` +
+ --- ## πŸƒβ€βž‘οΈ μ„€μΉ˜ 및 μ‹€ν–‰ @@ -105,4 +120,4 @@ pnpm dev # λΉŒλ“œ pnpm build -``` \ No newline at end of file +``` From 818c58edd0708d5b3ba400ea6a876c4093615609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9E=AC=EA=B2=BD?= <82097725+Shin-Yu-1@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:26:46 +0900 Subject: [PATCH 05/30] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c8955027..23eb36fb 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,9 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com --- -## πŸ“ FE 디렉토리 ꡬ쑰 +## πŸ“ 디렉토리 ꡬ쑰 + +### FE ```bash src/ β”œβ”€β”€ api/ # API μš”μ²­ ν•¨μˆ˜ μ •μ˜ (axios λ“±κ³Ό 연동) @@ -108,6 +110,65 @@ src/
+### BE +```bash +com.deepdirect.deepwebide_be/ +β”œβ”€β”€ chat/ # μ‹€μ‹œκ°„ μ±„νŒ… 도메인 +β”‚ β”œβ”€β”€ controller/ +β”‚ β”œβ”€β”€ domain/ +β”‚ β”œβ”€β”€ dto/ +β”‚ β”œβ”€β”€ repository/ +β”‚ β”œβ”€β”€ service/ +β”‚ β”œβ”€β”€ util/ +β”‚ └── websocket/ # WebSocket ν•Έλ“€λŸ¬ 및 λ©”μ‹œμ§€ 처리 +β”‚ +β”œβ”€β”€ file/ # 파일 μ—…λ‘œλ“œ 및 λ‹€μš΄λ‘œλ“œ (S3) +β”‚ β”œβ”€β”€ controller/ +β”‚ β”œβ”€β”€ domain/ +β”‚ β”œβ”€β”€ dto/ +β”‚ β”œβ”€β”€ repository/ +β”‚ └── service/ +β”‚ +β”œβ”€β”€ global/ # μ „μ—­ μ„€μ • 및 곡톡 λͺ¨λ“ˆ +β”‚ β”œβ”€β”€ config/ # Spring μ„€μ • (CORS, Swagger, Redis λ“±) +β”‚ β”œβ”€β”€ dto/ # 곡톡 응닡 DTO +β”‚ β”œβ”€β”€ exception/ # κΈ€λ‘œλ²Œ μ˜ˆμ™Έ 및 ν•Έλ“€λŸ¬ +β”‚ β”œβ”€β”€ security/ # JWT, OAuth κ΄€λ ¨ λ³΄μ•ˆ μ„€μ • +β”‚ └── util/ # μ „μ—­ μœ ν‹Έ 클래슀 +β”‚ +β”œβ”€β”€ history/ # μ½”λ“œ μ‹€ν–‰ 이λ ₯ 관리 +β”‚ β”œβ”€β”€ controller/ +β”‚ β”œβ”€β”€ domain/ +β”‚ β”œβ”€β”€ dto/ +β”‚ β”œβ”€β”€ repository/ +β”‚ └── service/ +β”‚ +β”œβ”€β”€ member/ # μ‚¬μš©μž 및 인증 도메인 +β”‚ β”œβ”€β”€ controller/ +β”‚ β”œβ”€β”€ domain/ +β”‚ β”œβ”€β”€ dto/ +β”‚ β”œβ”€β”€ repository/ +β”‚ β”œβ”€β”€ service/ +β”‚ └── util/ # λ‹‰λ„€μž„ μžλ™ 생성 μœ ν‹Έ +β”‚ +β”œβ”€β”€ repository/ # μ½”λ“œ μ €μž₯μ†Œ 도메인 +β”‚ β”œβ”€β”€ controller/ +β”‚ β”œβ”€β”€ domain/ +β”‚ β”œβ”€β”€ dto/ +β”‚ β”œβ”€β”€ repository/ +β”‚ β”œβ”€β”€ service/ +β”‚ └── util/ +β”‚ +└── sandbox/ # μƒŒλ“œλ°•μŠ€(μ½”λ“œ μ‹€ν–‰ ν™˜κ²½) 관리 +β”œβ”€β”€ config/ +β”œβ”€β”€ controller/ +β”œβ”€β”€ dto/ +β”œβ”€β”€ exception/ +└── service/ +``` + +
+ --- ## πŸƒβ€βž‘οΈ μ„€μΉ˜ 및 μ‹€ν–‰ From ce65c678607a45cf210343b2ffb76a5209ce72ec Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 10:48:43 +0900 Subject: [PATCH 06/30] =?UTF-8?q?fix(InvitationLinkForm):=20lock=20icon=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20import=20/=20DP-207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organisms/InvitationLinkForm/InvitationLinkForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/organisms/InvitationLinkForm/InvitationLinkForm.tsx b/src/components/organisms/InvitationLinkForm/InvitationLinkForm.tsx index 4e2ef77a..9ec6f9c0 100644 --- a/src/components/organisms/InvitationLinkForm/InvitationLinkForm.tsx +++ b/src/components/organisms/InvitationLinkForm/InvitationLinkForm.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react'; import PasswordInput from '@/components/atoms/Input/PasswordInput'; import InvitationFormActions from '@/components/molecules/InvitationFormActions/InvitationFormActions'; +import lockIcon from '../../../assets/icons/lock.svg'; import './InvitationLinkForm.scss'; @@ -35,7 +36,7 @@ const InvitationLinkForm: React.FC = ({
{/* μžλ¬Όμ‡  μ•„μ΄μ½˜ */}
- 잠금 μ•„μ΄μ½˜ + 잠금 μ•„μ΄μ½˜

μž…μž₯ μ½”λ“œ μž…λ ₯

From 5679f4f8ec13463b4f8eefee378a1c3184a25ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9E=AC=EA=B2=BD?= <82097725+Shin-Yu-1@users.noreply.github.com> Date: Sun, 3 Aug 2025 10:53:33 +0900 Subject: [PATCH 07/30] =?UTF-8?q?docs:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 23eb36fb..85dabe71 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,12 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com --- ## πŸ“Έ Screenshots +image +image +image +image + + From deb633c29210df70c3f6e59d6bb4a91d00203615 Mon Sep 17 00:00:00 2001 From: Jammanb0 <151705970+Jammanb0@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:15:15 +0900 Subject: [PATCH 08/30] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=B0=B8?= =?UTF-8?q?=EC=A1=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C=20/=20DP-20?= =?UTF-8?q?9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ChatMessage/ChatMessage.tsx | 60 +++++++++- .../FileTreeItem/FileTreeItem.module.scss | 106 ++++++++++++++++++ .../components/FileTreeItem/FileTreeItem.tsx | 43 +++++++ 3 files changed, 207 insertions(+), 2 deletions(-) diff --git a/src/features/Chat/components/ChatMessage/ChatMessage.tsx b/src/features/Chat/components/ChatMessage/ChatMessage.tsx index c2f7e194..06025280 100644 --- a/src/features/Chat/components/ChatMessage/ChatMessage.tsx +++ b/src/features/Chat/components/ChatMessage/ChatMessage.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import { useParams, useNavigate } from '@tanstack/react-router'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; +import { useTabStore } from '@/stores/tabStore'; import './ChatMessage.scss'; import { type ChatReceivedMessage } from '@/features/Chat/types'; @@ -15,12 +17,51 @@ dayjs.extend(timezone); dayjs.locale('ko'); const ChatMessage: React.FC = ({ message, isMyMessage }) => { + const params = useParams({ strict: false }); + const navigate = useNavigate(); + const { openFileByPath } = useTabStore(); + + // ν˜„μž¬ repo ID κ°€μ Έμ˜€κΈ° + const repoId = params.repoId as string; + // μ‹œκ°„ ν¬λ§·νŒ… ν•¨μˆ˜ const formatTime = (isoString: string) => { const time = dayjs.utc(isoString).tz('Asia/Seoul').format('HH:mm'); return time; }; + // 파일 경둜 클릭 ν•Έλ“€λŸ¬ + const handleFilePathClick = (filePath: string) => { + if (!repoId) { + console.warn('repoIdκ°€ μ—†μ–΄μ„œ νŒŒμΌμ„ μ—΄ 수 μ—†μŠ΅λ‹ˆλ‹€.'); + return; + } + + // 파일λͺ… μΆ”μΆœ (경둜의 λ§ˆμ§€λ§‰ λΆ€λΆ„) + const fileName = filePath.includes('/') ? filePath.split('/').pop() || 'untitled' : filePath; + + console.log('μ±„νŒ…μ—μ„œ 파일 경둜 클릭:', { + repoId, + filePath, + fileName, + }); + + // νƒ­μœΌλ‘œ 파일 μ—΄κΈ° + openFileByPath(repoId, filePath, fileName); + + // URL μ—…λ°μ΄νŠΈν•˜μ—¬ 파일 경둜 반영 + try { + navigate({ + to: '/$repoId', + params: { repoId }, + search: { file: filePath }, + replace: false, + }); + } catch (error) { + console.error('파일 경둜 λ„€λΉ„κ²Œμ΄μ…˜ μ‹€νŒ¨:', error); + } + }; + // λ©”μ‹œμ§€ λ‚΄μš©μ—μ„œ μ½”λ“œ μ°Έμ‘° νŒŒμ‹± const renderMessageContent = (content: string) => { // [[Ref: 파일 경둜]] νŒ¨ν„΄ @@ -35,9 +76,24 @@ const ChatMessage: React.FC = ({ message, isMyMessage }) => { parts.push(content.slice(lastIndex, match.index)); } + const filePath = match[1].trim(); + parts.push( - - [[Ref: {match[1]}]] + handleFilePathClick(filePath)} + role="button" + tabIndex={0} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleFilePathClick(filePath); + } + }} + title={`${filePath} 파일 μ—΄κΈ°`} + > + [[Ref: {filePath}]] ); diff --git a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss index 66d6b777..ad19d0df 100644 --- a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss +++ b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss @@ -49,6 +49,37 @@ } } + // νƒ­ μƒνƒœλ³„ λ―Έλ¬˜ν•œ μŠ€νƒ€μΌ λ³€κ²½ + &.hasTab { + // 탭이 μ—΄λ¦° 파일 - λ―Έλ¬˜ν•˜κ²Œ κ°•μ‘° + &.file { + font-weight: $font-weight-medium; + } + } + + &.activeTab { + // ν˜„μž¬ ν™œμ„± 탭인 파일 - ν˜Έλ²„ μƒνƒœμ™€ λ™μΌν•œ κΈ€μžμƒ‰ + λ―Έλ¬˜ν•œ 배경색 + &.file { + color: var(--filetree-text-hover); // ν˜Έλ²„ μƒνƒœμ™€ λ™μΌν•œ 색상 + background-color: var(--filetree-active-tab-subtle-bg); + + &:hover { + background-color: var(--filetree-active-tab-subtle-hover); + } + + // μ•„μ΄μ½˜λ„ ν˜Έλ²„ μƒνƒœμ™€ λ™μΌν•˜κ²Œ + .icon { + opacity: 1; + transform: scale(1.05); + + // 닀크λͺ¨λ“œμ—μ„œλ„ ν˜Έλ²„ μƒνƒœμ™€ 동일 + :global(.dark) & { + filter: brightness(0) invert(1) brightness(0.9) contrast(1.2); + } + } + } + } + // 파일과 폴더에 λ”°λ₯Έ μŠ€νƒ€μΌ 차이 &.folder { font-weight: $font-weight-medium; @@ -365,6 +396,38 @@ } } +// νƒ­ μƒνƒœ 인디케이터 μ»¨ν…Œμ΄λ„ˆ - 우츑 끝에 κ³ μ • +.tabStatusIndicators { + position: absolute; + top: 50%; + right: 8px; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + transform: translateY(-50%); + pointer-events: none; +} + +// νƒ­ μƒνƒœ 인디케이터듀 - 우츑 끝 κ³ μ • μœ„μΉ˜ +.activeIndicator { + width: 5px; + height: 5px; + border-radius: 50%; + background-color: var(--filetree-tab-active-dot); + box-shadow: 0 0 4px var(--filetree-tab-active-glow); + animation: subtle-pulse 2s ease-in-out infinite; +} + +.dirtyIndicator { + width: 4px; + height: 4px; + border-radius: 50%; + background-color: var(--filetree-tab-dirty-dot); + box-shadow: 0 0 3px var(--filetree-tab-dirty-glow); + animation: dirty-pulse 1.5s ease-in-out infinite; +} + // 파일/폴더 이름 .name { flex: 0 1 auto; @@ -498,6 +561,33 @@ } } +// νƒ­ μƒνƒœ μ• λ‹ˆλ©”μ΄μ…˜ - μ•„μ£Ό λ―Έλ¬˜ν•¨ +@keyframes subtle-pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.6; + transform: scale(0.8); + } +} + +@keyframes dirty-pulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.7; + transform: scale(0.9); + } +} + // μ•„λž˜μͺ½μ— λ‚˜νƒ€λ‚˜λŠ” 툴팁용 μ• λ‹ˆλ©”μ΄μ…˜ @keyframes tooltip-appear-below { from { @@ -540,6 +630,14 @@ --filetree-selected-indicator: #{$orange}; --filetree-editing-indicator: #{$yellow}; + // νƒ­ μƒνƒœ 색상 - λ―Έλ¬˜ν•¨ + --filetree-active-tab-subtle-bg: rgb(55 79 255 / 3%); + --filetree-active-tab-subtle-hover: rgb(55 79 255 / 6%); + --filetree-tab-active-dot: #{$blue-3}; + --filetree-tab-active-glow: rgb(55 79 255 / 40%); + --filetree-tab-dirty-dot: #{$orange}; + --filetree-tab-dirty-glow: rgb(255 107 53 / 40%); + // λ‚΄λΆ€ λ“œλž˜κ·Έμ•€λ“œλ‘­ 색상 --filetree-item-dragging: #{$gray-8}; --filetree-drag-border: #{$blue-3}; @@ -590,6 +688,14 @@ --filetree-selected-indicator: #{$orange}; --filetree-editing-indicator: #{$yellow}; + // νƒ­ μƒνƒœ 색상 - λ―Έλ¬˜ν•¨ + --filetree-active-tab-subtle-bg: rgb(55 79 255 / 8%); + --filetree-active-tab-subtle-hover: rgb(55 79 255 / 12%); + --filetree-tab-active-dot: #{$blue-3}; + --filetree-tab-active-glow: rgb(55 79 255 / 60%); + --filetree-tab-dirty-dot: #{$orange}; + --filetree-tab-dirty-glow: rgb(255 107 53 / 60%); + // λ‚΄λΆ€ λ“œλž˜κ·Έμ•€λ“œλ‘­ 색상 --filetree-item-dragging: #{$gray-5}; --filetree-drag-border: #{$blue-3}; diff --git a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx index 7dea01e1..bb29781a 100644 --- a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx +++ b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx @@ -1,6 +1,7 @@ import React from 'react'; import clsx from 'clsx'; import { getFolderIcon, getFileIcon } from '@/utils/fileExtensions'; +import { useTabStore } from '@/stores/tabStore'; import FileTreeContextMenu from '../FileTreeContextMenu/FileTreeContextMenu'; import InlineEdit from '../InlineEdit/InlineEdit'; import styles from './FileTreeItem.module.scss'; @@ -38,6 +39,29 @@ const FileTreeItem: React.FC = ({ onExternalDragLeave, onExternalDrop, }) => { + const { openTabs } = useTabStore(); + + // νƒ­ μƒνƒœ 확인 - 파일만 체크 + const getTabStatus = () => { + if (node.fileType === 'FOLDER') { + return { isOpen: false, isActive: false, isDirty: false }; + } + + const tab = openTabs.find( + tab => + tab.fileId === node.fileId || tab.path === node.path || tab.id.endsWith(`/${node.path}`) + ); + + return { + isOpen: !!tab && !tab.isDeleted && !tab.hasFileTreeMismatch, + isActive: !!tab && tab.isActive && !tab.isDeleted && !tab.hasFileTreeMismatch, + isDirty: !!tab && tab.isDirty && !tab.isDeleted && !tab.hasFileTreeMismatch, + tab, + }; + }; + + const tabStatus = getTabStatus(); + const handleClick = (e: React.MouseEvent) => { // νŽΈμ§‘ μ€‘μ΄κ±°λ‚˜ λ“œλž˜κ·Έ 쀑일 λ•ŒλŠ” 클릭 이벀트 λ¬΄μ‹œ if (isEditing || isDragging) { @@ -242,6 +266,10 @@ const FileTreeItem: React.FC = ({ [styles.canDrop]: canDrop && isDropTarget, [styles.cannotDrop]: !canDrop && isDropTarget, [styles.draggable]: !isEditing, + // νƒ­ μƒνƒœ 클래슀 - λ―Έλ¬˜ν•˜κ²Œ 적용 + [styles.hasTab]: tabStatus.isOpen, + [styles.activeTab]: tabStatus.isActive, + [styles.dirtyTab]: tabStatus.isDirty, // λ‚΄λΆ€ λ“œλ‘­ μœ„μΉ˜λ³„ 클래슀 [styles.dropBefore]: isDropTarget && getDropPosition?.(node.fileId.toString()) === 'before', @@ -270,6 +298,11 @@ const FileTreeItem: React.FC = ({ onDrop={handleCombinedDrop} // μ΅œμƒλ‹¨ 레벨 μ—¬λΆ€λ₯Ό data attribute둜 전달 data-is-top-level={isTopLevel} + title={ + tabStatus.isOpen + ? `${node.fileName}${tabStatus.isActive ? ' (ν™œμ„± νƒ­)' : ' (μ—΄λ¦° νƒ­)'}${tabStatus.isDirty ? ' (변경됨)' : ''}` + : node.fileName + } >
{node.fileType === 'FOLDER' && ( @@ -309,6 +342,16 @@ const FileTreeItem: React.FC = ({ validateInput={validateFileName} /> + {/* νƒ­ μƒνƒœ 인디케이터 - 우츑 끝에 κ³ μ • μœ„μΉ˜ */} + {node.fileType === 'FILE' && ( +
+ {tabStatus.isActive &&
} + {tabStatus.isDirty && ( +
+ )} +
+ )} + {/* μ™ΈλΆ€ 파일 λ“œλž˜κ·Έμ˜€λ²„ μƒνƒœ ν‘œμ‹œ */} {isExternalDragOver && (
From 75ca6eab5ad2162a3c84bc81b38b433f16decff1 Mon Sep 17 00:00:00 2001 From: Jammanb0 <151705970+Jammanb0@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:24:04 +0900 Subject: [PATCH 09/30] =?UTF-8?q?hotfix(ChatMessage):=20=EC=B1=84=ED=8C=85?= =?UTF-8?q?=20=EC=B0=B8=EC=A1=B0=20=EA=B8=B0=EB=8A=A5=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/ChatMessage/ChatMessage.tsx | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/features/Chat/components/ChatMessage/ChatMessage.tsx b/src/features/Chat/components/ChatMessage/ChatMessage.tsx index 06025280..e92abf88 100644 --- a/src/features/Chat/components/ChatMessage/ChatMessage.tsx +++ b/src/features/Chat/components/ChatMessage/ChatMessage.tsx @@ -4,6 +4,9 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import { useTabStore } from '@/stores/tabStore'; +import { useFileTreeQuery } from '@/features/Repo/fileTree/hooks/useFileTreeApi'; +import { findNodeByPath } from '@/features/Repo/fileTree/utils'; +import { useToast } from '@/hooks/common/useToast'; import './ChatMessage.scss'; import { type ChatReceivedMessage } from '@/features/Chat/types'; @@ -19,10 +22,15 @@ dayjs.locale('ko'); const ChatMessage: React.FC = ({ message, isMyMessage }) => { const params = useParams({ strict: false }); const navigate = useNavigate(); + const toast = useToast(); const { openFileByPath } = useTabStore(); // ν˜„μž¬ repo ID κ°€μ Έμ˜€κΈ° const repoId = params.repoId as string; + const repositoryId = repoId ? parseInt(repoId, 10) : 0; + + // 파일트리 데이터 κ°€μ Έμ˜€κΈ° (파일 쑴재 μ—¬λΆ€ ν™•μΈμš©) + const { data: fileTreeData } = useFileTreeQuery(repositoryId); // μ‹œκ°„ ν¬λ§·νŒ… ν•¨μˆ˜ const formatTime = (isoString: string) => { @@ -34,6 +42,28 @@ const ChatMessage: React.FC = ({ message, isMyMessage }) => { const handleFilePathClick = (filePath: string) => { if (!repoId) { console.warn('repoIdκ°€ μ—†μ–΄μ„œ νŒŒμΌμ„ μ—΄ 수 μ—†μŠ΅λ‹ˆλ‹€.'); + toast.error('μ €μž₯μ†Œ 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.'); + return; + } + + if (!fileTreeData || fileTreeData.length === 0) { + console.warn('파일트리 데이터가 μ—†μ–΄μ„œ 파일 쑴재 μ—¬λΆ€λ₯Ό 확인할 수 μ—†μŠ΅λ‹ˆλ‹€.'); + toast.error('파일 λͺ©λ‘μ„ λΆˆλŸ¬μ˜€λŠ” μ€‘μž…λ‹ˆλ‹€. μž μ‹œ ν›„ λ‹€μ‹œ μ‹œλ„ν•΄μ£Όμ„Έμš”.'); + return; + } + + // νŒŒμΌνŠΈλ¦¬μ—μ„œ ν•΄λ‹Ή 경둜의 파일이 μ‘΄μž¬ν•˜λŠ”μ§€ 확인 + const fileNode = findNodeByPath(fileTreeData, filePath); + + if (!fileNode) { + console.warn('νŒŒμΌνŠΈλ¦¬μ—μ„œ νŒŒμΌμ„ 찾을 수 μ—†μŒ:', filePath); + toast.error(`νŒŒμΌμ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€: ${filePath}`); + return; + } + + if (fileNode.fileType !== 'FILE') { + console.warn('ν΄λ”λŠ” νƒ­μœΌλ‘œ μ—΄ 수 μ—†μŒ:', filePath); + toast.error('ν΄λ”λŠ” μ—΄ 수 μ—†μŠ΅λ‹ˆλ‹€. 파일만 μ„ νƒν•΄μ£Όμ„Έμš”.'); return; } @@ -44,10 +74,12 @@ const ChatMessage: React.FC = ({ message, isMyMessage }) => { repoId, filePath, fileName, + fileId: fileNode.fileId, + fileExists: true, }); - // νƒ­μœΌλ‘œ 파일 μ—΄κΈ° - openFileByPath(repoId, filePath, fileName); + // νƒ­μœΌλ‘œ 파일 μ—΄κΈ° (fileId도 ν•¨κ»˜ 전달) + openFileByPath(repoId, filePath, fileName, fileNode.fileId); // URL μ—…λ°μ΄νŠΈν•˜μ—¬ 파일 경둜 반영 try { @@ -57,8 +89,11 @@ const ChatMessage: React.FC = ({ message, isMyMessage }) => { search: { file: filePath }, replace: false, }); + + toast.success(`${fileName} νŒŒμΌμ„ μ—΄μ—ˆμŠ΅λ‹ˆλ‹€.`); } catch (error) { console.error('파일 경둜 λ„€λΉ„κ²Œμ΄μ…˜ μ‹€νŒ¨:', error); + toast.error('νŒŒμΌμ„ μ—¬λŠ” 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); } }; From ec80f77e54ef07633a01c3d0721fc1ab0815e15c Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Sun, 3 Aug 2025 11:59:05 +0900 Subject: [PATCH 10/30] =?UTF-8?q?feat(RepoPage):=20CodeRunner=EC=97=90=20r?= =?UTF-8?q?epositoryName=20prop=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Repo/RepoPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Repo/RepoPage.tsx b/src/pages/Repo/RepoPage.tsx index 10068d2f..1773f738 100644 --- a/src/pages/Repo/RepoPage.tsx +++ b/src/pages/Repo/RepoPage.tsx @@ -359,7 +359,7 @@ export function RepoPage() {
- +
From a5c54271f8fd338a78628e0b8ea67cee2ffe89fe Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 12:42:07 +0900 Subject: [PATCH 11/30] =?UTF-8?q?fix(Chat):=20=EA=B2=80=EC=83=89=20?= =?UTF-8?q?=EA=B2=B0=EA=B3=BC=EA=B0=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20?= =?UTF-8?q?=EB=AC=B4=ED=95=9C=EB=A1=9C=EB=94=A9=20=EC=A0=9C=EA=B1=B0=20/?= =?UTF-8?q?=20DP-211?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Chat/ChatStompVer.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/features/Chat/ChatStompVer.tsx b/src/features/Chat/ChatStompVer.tsx index d14dcefe..2bed756f 100644 --- a/src/features/Chat/ChatStompVer.tsx +++ b/src/features/Chat/ChatStompVer.tsx @@ -186,12 +186,6 @@ const Chat: React.FC = ({ isConnected, connectedCount, messages,
)} - {displayMessages && displayMessages.length === 0 && ( -
- {searchResults ? '검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€.' : } -
- )} - {displayMessages.map((message, index) => { const shouldShowDate = shouldShowDateDivider(message, index); const isMyMessage = String(message.senderId) === currentUserId; From 5b0090704e9cd7be608e8a823d9d36c71ecaf844 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 12:43:36 +0900 Subject: [PATCH 12/30] =?UTF-8?q?fix(Chat):=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=97=86?= =?UTF-8?q?=EC=9D=84=20=EC=8B=9C=20=EB=AC=B4=ED=95=9C=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=ED=98=84=EC=83=81=20=EC=A0=9C=EA=B1=B0=20/=20DP-211?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Chat/ChatStompVer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/Chat/ChatStompVer.tsx b/src/features/Chat/ChatStompVer.tsx index 2bed756f..2bbe8007 100644 --- a/src/features/Chat/ChatStompVer.tsx +++ b/src/features/Chat/ChatStompVer.tsx @@ -42,7 +42,6 @@ const Chat: React.FC = ({ isConnected, connectedCount, messages, // ν˜„μž¬ μ‚¬μš©μž ID (λ©”μ‹œμ§€ λΉ„κ΅μš©) const currentUserId = getCurrentUserId(); - // const { data, isSuccess } = useGetPreviousChat(repoId); const { data, fetchNextPage, hasNextPage, isSuccess } = useGetChatMessagesInfinite(repoId); useEffect(() => { From 359ee03f67a4de7997b864a44dbc63a0ec0be2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9E=AC=EA=B2=BD?= <82097725+Shin-Yu-1@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:44:58 +0900 Subject: [PATCH 13/30] =?UTF-8?q?fix:=20README=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/README.md b/README.md index 85dabe71..3bd8289b 100644 --- a/README.md +++ b/README.md @@ -74,20 +74,6 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | μ—λŸ¬ 좔적 | Sentry (`sentry-spring-boot-starter`, logback 연동) | λŸ°νƒ€μž„ μ—λŸ¬ μ‹€μ‹œκ°„ 좔적 | | λ‘œκΉ… μœ ν‹Έλ¦¬ν‹° | Logback, Commons IO | λ‘œκΉ…, 파일 μœ ν‹Έλ¦¬ν‹° | -
- ---- - -## πŸ“Έ Screenshots -image -image -image -image - - - - -
--- From 04506ffc2adaebc21a5987289bd9f4e6abb86792 Mon Sep 17 00:00:00 2001 From: projectmiluju Date: Sun, 3 Aug 2025 13:45:44 +0900 Subject: [PATCH 14/30] =?UTF-8?q?fix(CodeRunner):=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=84=9C=EB=B2=84=20URL=EC=9D=84=20=EB=A1=9C=EC=BB=AC=ED=98=B8?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=97=90=EC=84=9C=20=EC=8B=A4=EC=A0=9C=20IP?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/CodeRunner/CodeRunner.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/CodeRunner/CodeRunner.tsx b/src/features/CodeRunner/CodeRunner.tsx index 0b159256..f84625e1 100644 --- a/src/features/CodeRunner/CodeRunner.tsx +++ b/src/features/CodeRunner/CodeRunner.tsx @@ -71,7 +71,7 @@ export function CodeRunner(props: CodeRunnerProps) { resp.status === 'SUCCESS' ? resp.output || resp.message : resp.error || resp.message; if (resp.port) { - const url = `http://localhost:${resp.port}`; + const url = `http://3.39.22.178:${resp.port}`; output = ( Date: Sun, 3 Aug 2025 12:09:58 +0900 Subject: [PATCH 15/30] =?UTF-8?q?refactor(CodeRunner):=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=ED=83=80=EC=9E=85=20=EC=84=A0=EC=96=B8=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20/=20DP-210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/CodeRunner/CodeRunner.tsx | 17 +++-------------- src/features/CodeRunner/types.ts | 10 +++++----- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/features/CodeRunner/CodeRunner.tsx b/src/features/CodeRunner/CodeRunner.tsx index f84625e1..72303167 100644 --- a/src/features/CodeRunner/CodeRunner.tsx +++ b/src/features/CodeRunner/CodeRunner.tsx @@ -1,22 +1,11 @@ import { useState, useRef, useEffect } from 'react'; -import type { KeyboardEvent } from 'react'; +import type { KeyboardEvent, ReactNode } from 'react'; import { useThemeStore } from '@/stores/themeStore'; import { useCodeRunnerExecute } from '@/features/Repo/codeRunner/hooks/useCodeRunnerExecute'; import { useCodeRunnerStop } from '@/features/Repo/codeRunner/hooks/useCodeRunnerStop'; import { useCodeRunnerLogs } from '@/features/Repo/codeRunner/hooks/useCodeRunnerLogs'; +import type { CodeRunnerProps, CommandHistory } from './types'; import './CodeRunner.scss'; -import type { JSX } from 'react/jsx-runtime'; - -export interface CodeRunnerProps { - repoId?: number | string; - repositoryName?: string; -} - -interface CommandHistory { - command: string; - output: string | JSX.Element; - timestamp: Date; -} export function CodeRunner(props: CodeRunnerProps) { const [commandHistory, setCommandHistory] = useState([ @@ -67,7 +56,7 @@ export function CodeRunner(props: CodeRunnerProps) { codeRunnerExecute.mutate(undefined, { onSuccess: resp => { - let output: string | JSX.Element = + let output: string | ReactNode = resp.status === 'SUCCESS' ? resp.output || resp.message : resp.error || resp.message; if (resp.port) { diff --git a/src/features/CodeRunner/types.ts b/src/features/CodeRunner/types.ts index a7f8da45..9183798f 100644 --- a/src/features/CodeRunner/types.ts +++ b/src/features/CodeRunner/types.ts @@ -1,14 +1,14 @@ +import type { ReactNode } from 'react'; + export interface CommandHistory { command: string; - output: string; + output: string | ReactNode; timestamp: Date; } export interface CodeRunnerProps { - repoId?: string; - onCommandExecute?: (command: string) => Promise | string; - onClose?: () => void; - initialHistory?: CommandHistory[]; + repoId?: number | string; + repositoryName?: string; } export interface CodeRunnerState { From 6a5ed6390c47de89add28165f9cbb13051503964 Mon Sep 17 00:00:00 2001 From: Jammanb0 <151705970+Jammanb0@users.noreply.github.com> Date: Sun, 3 Aug 2025 13:09:11 +0900 Subject: [PATCH 16/30] =?UTF-8?q?feat:=20=ED=84=B0=EB=AF=B8=EB=84=90=20Yjs?= =?UTF-8?q?=EC=97=90=20=EC=97=B0=EA=B2=B0=20/=20DP-210?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cspell.json | 1 + src/features/CodeRunner/CodeRunner.scss | 58 ++++ src/features/CodeRunner/CodeRunner.tsx | 269 +++++++++++----- src/features/CodeRunner/types.ts | 3 + src/hooks/repo/useYjsCodeRunner.ts | 412 ++++++++++++++++++++++++ src/pages/Repo/RepoPage.tsx | 10 +- 6 files changed, 676 insertions(+), 77 deletions(-) create mode 100644 src/hooks/repo/useYjsCodeRunner.ts diff --git a/cspell.json b/cspell.json index ce454e29..45d393ec 100644 --- a/cspell.json +++ b/cspell.json @@ -24,6 +24,7 @@ ], "words": [ "Avenir", + "coderunner", "eslint", "filetree", "partialize", diff --git a/src/features/CodeRunner/CodeRunner.scss b/src/features/CodeRunner/CodeRunner.scss index 2f8d8b51..1d6b9794 100644 --- a/src/features/CodeRunner/CodeRunner.scss +++ b/src/features/CodeRunner/CodeRunner.scss @@ -22,6 +22,7 @@ right: 17px; z-index: 1; display: flex; + align-items: center; gap: 4px; } @@ -51,6 +52,63 @@ } } +.code-runner__collaboration-status { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 50%; + font-size: 16px; + transition: all 0.3s ease; + + &.connected { + color: #10b981; + } + + &.disconnected { + color: #6b7280; + } + + &.error { + color: #ef4444; + animation: shake 0.5s ease-in-out; + } +} + +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + + 25% { + transform: translateX(-2px); + } + + 75% { + transform: translateX(2px); + } +} + +.code-runner__status { + position: absolute; + top: 50px; + right: 17px; + z-index: 2; + padding: 4px 8px; + border-radius: 4px; + font-size: 10px; + color: #6b7280; + background-color: rgb(255 255 255 / 90%); + backdrop-filter: blur(4px); + + .dark & { + color: #9ca3af; + background-color: rgb(0 0 0 / 70%); + } +} + .code-runner__content { flex: 1; padding: 26px 20px 20px; diff --git a/src/features/CodeRunner/CodeRunner.tsx b/src/features/CodeRunner/CodeRunner.tsx index 72303167..aba85eca 100644 --- a/src/features/CodeRunner/CodeRunner.tsx +++ b/src/features/CodeRunner/CodeRunner.tsx @@ -4,11 +4,12 @@ import { useThemeStore } from '@/stores/themeStore'; import { useCodeRunnerExecute } from '@/features/Repo/codeRunner/hooks/useCodeRunnerExecute'; import { useCodeRunnerStop } from '@/features/Repo/codeRunner/hooks/useCodeRunnerStop'; import { useCodeRunnerLogs } from '@/features/Repo/codeRunner/hooks/useCodeRunnerLogs'; +import { useYjsCodeRunner } from '@/hooks/repo/useYjsCodeRunner'; import type { CodeRunnerProps, CommandHistory } from './types'; import './CodeRunner.scss'; export function CodeRunner(props: CodeRunnerProps) { - const [commandHistory, setCommandHistory] = useState([ + const [localCommandHistory, setLocalCommandHistory] = useState([ { command: '', output: 'Hello DeepWebIDE!', @@ -19,43 +20,93 @@ export function CodeRunner(props: CodeRunnerProps) { const inputRef = useRef(null); const contentRef = useRef(null); - // μ‹€μ‹œκ°„ 둜그 ν•œ 쀄씩 λˆ„μ μš© const [isStreamingLogs, setIsStreamingLogs] = useState(false); const [streamedLogLines, setStreamedLogLines] = useState([]); - // 닀크 λͺ¨λ“œ μ΄ˆκΈ°ν™” const { initializeTheme } = useThemeStore(); useEffect(() => { initializeTheme(); }, [initializeTheme]); - // μ‹€ν–‰/쀑지 μ»€μŠ€ν…€ ν›… const codeRunnerExecute = useCodeRunnerExecute(props.repoId); const codeRunnerStop = useCodeRunnerStop(props.repoId); - - // logs: enabled=false, refetch둜 직접 μš”μ²­ const { data: logsData, refetch: refetchLogs } = useCodeRunnerLogs(props.repoId); - // logs λͺ…λ Ήμ–΄ 처리: refetchLogs() 호좜, streaming μ€€λΉ„ + const roomId = props.repoId ? `repo-${props.repoId}` : ''; + + console.log(`[CodeRunner] λ Œλ”λ§:`, { + repoId: props.repoId, + roomId, + enableCollaboration: props.enableCollaboration, + userId: props.userId, + userName: props.userName, + }); + + const { + isConnected, + error: collaborationError, + commandHistory: collaborativeHistory, + broadcastCommand, + } = useYjsCodeRunner({ + roomId, + userId: props.userId || `user-${Date.now()}`, + userName: props.userName || 'Anonymous', + enabled: Boolean(props.enableCollaboration && props.repoId), + }); + + const commandHistory = props.enableCollaboration ? collaborativeHistory : localCommandHistory; + + console.log(`[CodeRunner] μƒνƒœ:`, { + enableCollaboration: props.enableCollaboration, + isConnected, + collaborationError, + localHistoryLength: localCommandHistory.length, + collaborativeHistoryLength: collaborativeHistory.length, + currentHistoryLength: commandHistory.length, + }); + const executeCommand = (command: string) => { if (!command) return; + + console.log(`[CodeRunner] λͺ…λ Ήμ–΄ μ‹€ν–‰: ${command}`); + if (command === 'logs') { setStreamedLogLines([]); - setCommandHistory(prev => [ - ...prev, - { command, output: '둜그 λΆˆλŸ¬μ˜€λŠ” 쀑...', timestamp: new Date() }, - ]); + + if (props.enableCollaboration && isConnected) { + console.log(`[CodeRunner] ν˜‘μ—… λͺ¨λ“œ logs λΈŒλ‘œλ“œμΊμŠ€νŠΈ`); + broadcastCommand(command, '둜그 λΆˆλŸ¬μ˜€λŠ” 쀑...', new Date()); + } else { + console.log(`[CodeRunner] 둜컬 λͺ¨λ“œ logs μ‹€ν–‰`); + setLocalCommandHistory(prev => [ + ...prev, + { command, output: '둜그 λΆˆλŸ¬μ˜€λŠ” 쀑...', timestamp: new Date() }, + ]); + } + setCurrentCommand(''); refetchLogs(); setIsStreamingLogs(true); return; } - setCommandHistory(prev => [...prev, { command, output: 'μ‹€ν–‰ 쀑...', timestamp: new Date() }]); + if (props.enableCollaboration && isConnected) { + console.log(`[CodeRunner] ν˜‘μ—… λͺ¨λ“œ λͺ…λ Ήμ–΄ λΈŒλ‘œλ“œμΊμŠ€νŠΈ: ${command}`); + broadcastCommand(command, 'μ‹€ν–‰ 쀑...', new Date()); + } else { + console.log(`[CodeRunner] 둜컬 λͺ¨λ“œ λͺ…λ Ήμ–΄ μ‹€ν–‰: ${command}`); + setLocalCommandHistory(prev => [ + ...prev, + { command, output: 'μ‹€ν–‰ 쀑...', timestamp: new Date() }, + ]); + } + setCurrentCommand(''); codeRunnerExecute.mutate(undefined, { onSuccess: resp => { + console.log(`[CodeRunner] μ‹€ν–‰ 성곡:`, resp); + let output: string | ReactNode = resp.status === 'SUCCESS' ? resp.output || resp.message : resp.error || resp.message; @@ -73,55 +124,75 @@ export function CodeRunner(props: CodeRunnerProps) { ); } - setCommandHistory(prev => [ - ...prev.slice(0, -1), - { - command, - output, - timestamp: new Date(), - }, - ]); + if (props.enableCollaboration && isConnected) { + const outputText = + typeof output === 'string' + ? output + : resp.port + ? `http://localhost:${resp.port}` + : 'Link generated'; + console.log(`[CodeRunner] ν˜‘μ—… λͺ¨λ“œ κ²°κ³Ό λΈŒλ‘œλ“œμΊμŠ€νŠΈ: ${outputText}`); + broadcastCommand(command, outputText, new Date()); + } else { + console.log(`[CodeRunner] 둜컬 λͺ¨λ“œ κ²°κ³Ό μ €μž₯`); + setLocalCommandHistory(prev => [ + ...prev.slice(0, -1), + { + command, + output, + timestamp: new Date(), + }, + ]); + } - // β˜…β˜…β˜… run μ‹€ν–‰ ν›„ μžλ™ logs 호좜 if (command === 'run' || command === '') { - // κΈ°μ‘΄ logs 좜λ ₯ μ§€μš°κ³  μƒˆλ‘œ setTimeout(() => { executeCommand('logs'); }, 1000); } }, onError: (e: unknown) => { + console.error(`[CodeRunner] μ‹€ν–‰ μ‹€νŒ¨:`, e); + let errorMessage = 'μ‹€νŒ¨'; if (typeof e === 'object' && e !== null) { const err = e as { response?: { data?: { message?: string } }; message?: string }; errorMessage = err.response?.data?.message || err.message || 'μ‹€νŒ¨'; } - setCommandHistory(prev => [ - ...prev.slice(0, -1), - { - command, - output: errorMessage, - timestamp: new Date(), - }, - ]); + + if (props.enableCollaboration && isConnected) { + console.log(`[CodeRunner] ν˜‘μ—… λͺ¨λ“œ 였λ₯˜ λΈŒλ‘œλ“œμΊμŠ€νŠΈ: ${errorMessage}`); + broadcastCommand(command, errorMessage, new Date()); + } else { + console.log(`[CodeRunner] 둜컬 λͺ¨λ“œ 였λ₯˜ μ €μž₯`); + setLocalCommandHistory(prev => [ + ...prev.slice(0, -1), + { + command, + output: errorMessage, + timestamp: new Date(), + }, + ]); + } }, }); }; - // logsDataκ°€ 였면 ν•œ 쀄씩 streaming useEffect(() => { if (!isStreamingLogs || !logsData?.logs) return; + console.log(`[CodeRunner] 둜그 슀트리밍 μ‹œμž‘, 라인 수: ${logsData.logs.split('\n').length}`); + const lines = logsData.logs.split('\n'); let idx = 0; - // "둜그 λΆˆλŸ¬μ˜€λŠ” 쀑..." 제거 - setCommandHistory(prev => prev.slice(0, -1)); - setStreamedLogLines([]); // μ΄ˆκΈ°ν™” + if (!props.enableCollaboration || !isConnected) { + setLocalCommandHistory(prev => prev.slice(0, -1)); + } + setStreamedLogLines([]); const interval = setInterval(() => { setStreamedLogLines(prev => { - // μ•ˆμ „ν•˜κ²Œ lines[idx]κ°€ undefined일 경우 빈 λ¬Έμžμ—΄λ‘œ λŒ€μ²΄ const line = idx < lines.length && typeof lines[idx] === 'string' ? lines[idx] : ''; return [...prev, line]; }); @@ -129,65 +200,87 @@ export function CodeRunner(props: CodeRunnerProps) { if (idx >= lines.length) { clearInterval(interval); setIsStreamingLogs(false); + console.log(`[CodeRunner] 둜그 슀트리밍 μ™„λ£Œ`); } }, 60); return () => clearInterval(interval); - }, [isStreamingLogs, logsData]); + }, [isStreamingLogs, logsData, props.enableCollaboration, isConnected]); - // streamedLogLines λ³€κ²½ μ‹œ commandHistory logs에 λˆ„μ  반영 useEffect(() => { if (streamedLogLines.length === 0) return; - setCommandHistory(prev => [ - // κΈ°μ‘΄ logs λͺ…λ Ή κ²°κ³ΌλŠ” 지움 - ...prev.filter(item => item.command !== 'logs'), - { - command: 'logs', - output: ( -
- {streamedLogLines.map((line, idx) => ( -
- {typeof line === 'string' ? line.trimStart() : ''} -
- ))} + const logsOutput = ( +
+ {streamedLogLines.map((line, idx) => ( +
+ {typeof line === 'string' ? line.trimStart() : ''}
- ), - timestamp: new Date(), - }, - ]); - }, [streamedLogLines]); + ))} +
+ ); + if (props.enableCollaboration && isConnected) { + console.log(`[CodeRunner] ν˜‘μ—… λͺ¨λ“œ 둜그 좜λ ₯ λΈŒλ‘œλ“œμΊμŠ€νŠΈ`); + broadcastCommand('logs', `Logs displayed (${streamedLogLines.length} lines)`, new Date()); + } else { + console.log(`[CodeRunner] 둜컬 λͺ¨λ“œ 둜그 좜λ ₯ μ €μž₯`); + setLocalCommandHistory(prev => [ + ...prev.filter(item => item.command !== 'logs'), + { + command: 'logs', + output: logsOutput, + timestamp: new Date(), + }, + ]); + } + }, [streamedLogLines, props.enableCollaboration, isConnected, broadcastCommand]); + + // πŸ”§ μˆ˜μ •: stop 이쀑 처리 문제 ν•΄κ²° const handleStop = () => { - setCommandHistory(prev => [ - ...prev, - { command: 'stop', output: '쀑지 쀑...', timestamp: new Date() }, - ]); + console.log(`[CodeRunner] 쀑지 λͺ…λ Ήμ–΄ μ‹€ν–‰`); + + // πŸ”§ μ¦‰μ‹œ λΈŒλ‘œλ“œμΊμŠ€νŠΈν•˜μ§€ μ•Šκ³  API 호좜만 codeRunnerStop.mutate(undefined, { onSuccess: resp => { - setCommandHistory(prev => [ - ...prev.slice(0, -1), - { - command: 'stop', - output: resp.message, - timestamp: new Date(), - }, - ]); + console.log(`[CodeRunner] 쀑지 성곡:`, resp); + + // πŸ”§ 성곡 μ‹œμ—λ§Œ λΈŒλ‘œλ“œμΊμŠ€νŠΈ/μ €μž₯ + if (props.enableCollaboration && isConnected) { + broadcastCommand('stop', resp.message, new Date()); + } else { + setLocalCommandHistory(prev => [ + ...prev, + { + command: 'stop', + output: resp.message, + timestamp: new Date(), + }, + ]); + } }, onError: e => { + console.error(`[CodeRunner] 쀑지 μ‹€νŒ¨:`, e); + let errorMessage = '쀑지 μ‹€νŒ¨'; if (typeof e === 'object' && e !== null) { const err = e as { response?: { data?: { message?: string } }; message?: string }; errorMessage = err.response?.data?.message || err.message || '쀑지 μ‹€νŒ¨'; } - setCommandHistory(prev => [ - ...prev.slice(0, -1), - { - command: 'stop', - output: errorMessage, - timestamp: new Date(), - }, - ]); + + // πŸ”§ μ‹€νŒ¨ μ‹œμ—λ§Œ λΈŒλ‘œλ“œμΊμŠ€νŠΈ/μ €μž₯ + if (props.enableCollaboration && isConnected) { + broadcastCommand('stop', errorMessage, new Date()); + } else { + setLocalCommandHistory(prev => [ + ...prev, + { + command: 'stop', + output: errorMessage, + timestamp: new Date(), + }, + ]); + } }, }); }; @@ -204,9 +297,15 @@ export function CodeRunner(props: CodeRunnerProps) { } }, [commandHistory]); + const getConnectionStatusText = () => { + if (!props.enableCollaboration) return null; + if (collaborationError) return 'μ—°κ²° 였λ₯˜'; + if (isConnected) return '연결됨'; + return 'μ—°κ²° 쀑...'; + }; + return (
- {/* μ œμ–΄ μ„Ήμ…˜ */}
+ {props.enableCollaboration && ( +
+ {collaborationError ? '!' : isConnected ? '●' : 'β—‹'} +
+ )}
- {/* 터미널 μ½˜ν…μΈ  */} + {props.enableCollaboration && ( +
+ {getConnectionStatusText()} + {/* πŸ”§ 디버그 정보 μΆ”κ°€ */} +
+ User: {props.userId} ({props.userName}) +
+
+ )} +
{commandHistory.map((item, index) => ( diff --git a/src/features/CodeRunner/types.ts b/src/features/CodeRunner/types.ts index 9183798f..97fdda67 100644 --- a/src/features/CodeRunner/types.ts +++ b/src/features/CodeRunner/types.ts @@ -9,6 +9,9 @@ export interface CommandHistory { export interface CodeRunnerProps { repoId?: number | string; repositoryName?: string; + enableCollaboration?: boolean; + userId?: string; + userName?: string; } export interface CodeRunnerState { diff --git a/src/hooks/repo/useYjsCodeRunner.ts b/src/hooks/repo/useYjsCodeRunner.ts new file mode 100644 index 00000000..2d2718e5 --- /dev/null +++ b/src/hooks/repo/useYjsCodeRunner.ts @@ -0,0 +1,412 @@ +import { useEffect, useRef, useCallback, useState } from 'react'; +import * as Y from 'yjs'; +import { WebsocketProvider } from 'y-websocket'; +import { useCollaborationStore } from '@/stores/collaborationStore'; +import type { CommandHistory } from '@/features/CodeRunner/types'; + +interface CodeRunnerCollaborationConfig { + roomId: string; + userId: string; + userName: string; + enabled?: boolean; +} + +interface CodeRunnerConnectionData { + doc: Y.Doc; + provider: WebsocketProvider; + yArray: Y.Array; + activeUsers: Set; + cleanupTimer?: NodeJS.Timeout; + reconnectAttempts: number; + maxReconnectAttempts: number; + isDestroyed: boolean; +} + +const codeRunnerConnections = new Map(); + +const getWebSocketUrl = (): string => { + return import.meta.env.VITE_YJS_WEBSOCKET_URL || 'ws://localhost:1234'; +}; + +const cleanupCodeRunnerConnection = (roomId: string) => { + const connection = codeRunnerConnections.get(roomId); + if (!connection) return; + + console.log(`[CodeRunner] μ—°κ²° 정리: ${roomId}`); + + try { + connection.isDestroyed = true; + + if (connection.cleanupTimer) { + clearTimeout(connection.cleanupTimer); + } + + connection.provider.disconnect(); + connection.provider.destroy(); + connection.doc.destroy(); + codeRunnerConnections.delete(roomId); + + console.log(`[CodeRunner] μ—°κ²° 정리 μ™„λ£Œ: ${roomId}`); + } catch (error) { + console.error(`[CodeRunner] μ—°κ²° 정리 μ‹€νŒ¨: ${roomId}`, error); + } +}; + +export const useYjsCodeRunner = ({ + roomId, + userId, + userName, + enabled = true, +}: CodeRunnerCollaborationConfig) => { + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + const [commandHistory, setCommandHistory] = useState([]); + + const currentUserIdRef = useRef(''); + const isInitializedRef = useRef(false); + const cleanupInProgressRef = useRef(false); + const reconnectTimeoutRef = useRef(null); + + const { setConnectionStatus, addUser, clearUsers } = useCollaborationStore(); + + const cleanup = useCallback(() => { + if (cleanupInProgressRef.current) return; + cleanupInProgressRef.current = true; + + try { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + reconnectTimeoutRef.current = null; + } + + const connection = codeRunnerConnections.get(roomId); + if (connection && currentUserIdRef.current) { + connection.isDestroyed = true; + connection.activeUsers.delete(currentUserIdRef.current); + console.log( + `[CodeRunner] μ‚¬μš©μž 제거: ${currentUserIdRef.current}, 남은 μ‚¬μš©μž: ${connection.activeUsers.size}` + ); + + if (connection.activeUsers.size === 0) { + if (connection.cleanupTimer) { + clearTimeout(connection.cleanupTimer); + } + + connection.cleanupTimer = setTimeout(() => { + cleanupCodeRunnerConnection(roomId); + }, 60000); + console.log(`[CodeRunner] cleanup timer μ„€μ •: ${roomId}`); + } + } + + setConnectionStatus(false); + setIsConnected(false); + setError(null); + clearUsers(); + currentUserIdRef.current = ''; + } catch (cleanupError) { + console.error(`[CodeRunner] 정리 쀑 였λ₯˜: ${roomId}`, cleanupError); + } finally { + isInitializedRef.current = false; + cleanupInProgressRef.current = false; + } + }, [roomId, setConnectionStatus, clearUsers]); + + const broadcastCommand = useCallback( + (command: string, output: string, timestamp: Date) => { + const connection = codeRunnerConnections.get(roomId); + if (!connection || connection.isDestroyed) { + console.warn(`[CodeRunner] 연결을 찾을 수 μ—†κ±°λ‚˜ 파괴됨: ${roomId}`); + return; + } + + const commandData = { + id: `${userId}-${Date.now()}-${Math.random()}`, + userId, + userName, + command, + output, + timestamp: timestamp.toISOString(), + type: 'command', + }; + + console.log(`[CodeRunner] λͺ…λ Ήμ–΄ λΈŒλ‘œλ“œμΊμŠ€νŠΈ:`, commandData); + + try { + connection.yArray.push([commandData]); + console.log(`[CodeRunner] Y.Array ν˜„μž¬ 길이: ${connection.yArray.length}`); + } catch (error) { + console.error(`[CodeRunner] λΈŒλ‘œλ“œμΊμŠ€νŠΈ μ‹€νŒ¨:`, error); + } + }, + [roomId, userId, userName] + ); + + const scheduleReconnect = useCallback( + (connection: CodeRunnerConnectionData, delay: number = 3000) => { + if ( + connection.isDestroyed || + connection.reconnectAttempts >= connection.maxReconnectAttempts + ) { + console.log( + `[CodeRunner] μž¬μ—°κ²° 쀑단: ${roomId} (μ‹œλ„: ${connection.reconnectAttempts}/${connection.maxReconnectAttempts})` + ); + setError('연결에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. νŽ˜μ΄μ§€λ₯Ό μƒˆλ‘œκ³ μΉ¨ν•΄μ£Όμ„Έμš”.'); + return; + } + + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current); + } + + reconnectTimeoutRef.current = setTimeout(() => { + if ( + !connection.isDestroyed && + connection.provider.shouldConnect && + !connection.provider.wsconnected + ) { + console.log( + `[CodeRunner] μž¬μ—°κ²° μ‹œλ„ ${connection.reconnectAttempts + 1}/${connection.maxReconnectAttempts}: ${roomId}` + ); + connection.reconnectAttempts++; + connection.provider.connect(); + } + }, delay); + }, + [roomId] + ); + + const initialize = useCallback(async () => { + if (!roomId || !enabled || isInitializedRef.current || cleanupInProgressRef.current) { + console.log(`[CodeRunner] μ΄ˆκΈ°ν™” κ±΄λ„ˆλ›°κΈ°:`, { + roomId, + enabled, + isInitialized: isInitializedRef.current, + }); + return; + } + + try { + console.log(`[CodeRunner] ν˜‘μ—… μ΄ˆκΈ°ν™” μ‹œμž‘: ${roomId}, μ‚¬μš©μž: ${userId}(${userName})`); + + currentUserIdRef.current = userId; + + let connection = codeRunnerConnections.get(roomId); + + if (!connection) { + const doc = new Y.Doc(); + const yArray = doc.getArray('coderunner-commands'); + const wsUrl = getWebSocketUrl(); + const fullRoomId = `coderunner-${roomId}`; + + console.log(`[CodeRunner] μƒˆ μ—°κ²° 생성: ${wsUrl}/${fullRoomId}`); + + const provider = new WebsocketProvider(wsUrl, fullRoomId, doc, { + connect: true, + maxBackoffTime: 30000, + resyncInterval: 30000, + }); + + connection = { + doc, + provider, + yArray, + activeUsers: new Set(), + reconnectAttempts: 0, + maxReconnectAttempts: 5, + isDestroyed: false, + }; + + provider.on('status', (event: { status: string }) => { + if (connection!.isDestroyed) return; + + const connected = event.status === 'connected'; + console.log(`[CodeRunner] WebSocket μƒνƒœ λ³€κ²½: ${event.status} (${roomId})`); + + setConnectionStatus(connected); + setIsConnected(connected); + + if (connected) { + setError(null); + connection!.reconnectAttempts = 0; + + const currentUser = { + id: userId, + name: userName, + color: '#' + Math.floor(Math.random() * 16777215).toString(16), + lastSeen: Date.now(), + }; + + provider.awareness.setLocalStateField('user', currentUser); + console.log(`[CodeRunner] Awareness μ„€μ •:`, currentUser); + } else { + if (event.status === 'disconnected' && !connection!.isDestroyed) { + setError('연결이 λŠμ–΄μ‘ŒμŠ΅λ‹ˆλ‹€. μž¬μ—°κ²° 쀑...'); + scheduleReconnect(connection!, 3000); + } + } + }); + + provider.on('connection-close', event => { + if (connection!.isDestroyed) return; + console.log(`[CodeRunner] WebSocket μ—°κ²° λ‹«νž˜:`, event); + + if (event && (event.code === 1003 || event.code === 1008)) { + console.log(`[CodeRunner] μ„œλ²„μ—μ„œ μ—°κ²° 거뢀됨 (μ½”λ“œ: ${event.code})`); + setError('μ„œλ²„μ—μ„œ 연결을 κ±°λΆ€ν–ˆμŠ΅λ‹ˆλ‹€.'); + connection!.reconnectAttempts = connection!.maxReconnectAttempts; + } else { + scheduleReconnect(connection!, 5000); + } + }); + + provider.on('connection-error', event => { + if (connection!.isDestroyed) return; + console.error(`[CodeRunner] WebSocket μ—°κ²° 였λ₯˜:`, event); + setError('μ—°κ²° 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.'); + }); + + const handleAwarenessChange = () => { + if (connection!.isDestroyed) return; + + try { + const states = provider.awareness.getStates(); + console.log(`[CodeRunner] Awareness λ³€κ²½, 총 ν΄λΌμ΄μ–ΈνŠΈ: ${states.size}`); + + clearUsers(); + + for (const [clientId, state] of states.entries()) { + const awarenessState = state as { + user?: { id: string; name: string; color: string }; + }; + + if (awarenessState.user && clientId !== provider.awareness.clientID) { + const user = { + id: awarenessState.user.id, + name: awarenessState.user.name, + color: awarenessState.user.color, + lastSeen: Date.now(), + }; + addUser(user); + console.log(`[CodeRunner] μ‚¬μš©μž μΆ”κ°€:`, user); + } + } + } catch (awarenessError) { + console.error('[CodeRunner] Awareness 처리 였λ₯˜:', awarenessError); + } + }; + + provider.awareness.on('change', handleAwarenessChange); + + const handleYArrayChange = () => { + if (connection!.isDestroyed) return; + + try { + const commands = connection!.yArray.toArray() as Array<{ + id: string; + userId: string; + userName: string; + command: string; + output: string; + timestamp: string; + type: string; + }>; + + console.log(`[CodeRunner] Y.Array λ³€κ²½, 총 λͺ…λ Ήμ–΄: ${commands.length}`); + + // πŸ”§ μˆ˜μ •: μ‚¬μš©μž ꡬ뢄 둜직 κ°œμ„  + const formattedHistory: CommandHistory[] = commands.map(cmd => { + console.log(`[CodeRunner] λͺ…λ Ήμ–΄ 처리:`, { + cmdUserId: cmd.userId, + currentUserId: userId, + isCurrentUser: cmd.userId === userId, + userName: cmd.userName, + command: cmd.command, + }); + + return { + command: cmd.command, + // πŸ”§ λͺ¨λ“  λͺ…령어에 μ‚¬μš©μžλͺ… ν‘œμ‹œ (본인 λͺ…령어도 ꡬ뢄 κ°€λŠ₯ν•˜λ„λ‘) + output: `[${cmd.userName}] ${cmd.output}`, + timestamp: new Date(cmd.timestamp), + }; + }); + + setCommandHistory(formattedHistory); + console.log(`[CodeRunner] λͺ…λ Ήμ–΄ νžˆμŠ€ν† λ¦¬ μ—…λ°μ΄νŠΈ:`, formattedHistory.length); + } catch (arrayError) { + console.error('[CodeRunner] Array λ³€κ²½ 처리 였λ₯˜:', arrayError); + } + }; + + connection.yArray.observe(handleYArrayChange); + codeRunnerConnections.set(roomId, connection); + + console.log(`[CodeRunner] μ—°κ²° 맡에 μ €μž₯: ${roomId}`); + } + + if (connection.cleanupTimer) { + clearTimeout(connection.cleanupTimer); + connection.cleanupTimer = undefined; + console.log(`[CodeRunner] κΈ°μ‘΄ cleanup timer μ·¨μ†Œ: ${roomId}`); + } + + connection.activeUsers.add(userId); + console.log(`[CodeRunner] μ‚¬μš©μž μΆ”κ°€: ${userId}, 총 μ‚¬μš©μž: ${connection.activeUsers.size}`); + + const connected = connection.provider.wsconnected; + setConnectionStatus(connected); + setIsConnected(connected); + + if (connected) { + const currentUser = { + id: userId, + name: userName, + color: '#' + Math.floor(Math.random() * 16777215).toString(16), + lastSeen: Date.now(), + }; + connection.provider.awareness.setLocalStateField('user', currentUser); + } + + isInitializedRef.current = true; + console.log(`[CodeRunner] ν˜‘μ—… μ΄ˆκΈ°ν™” μ™„λ£Œ: ${roomId}`); + } catch (initError) { + console.error(`[CodeRunner] μ΄ˆκΈ°ν™” μ‹€νŒ¨: ${roomId}`, initError); + setError('ν˜‘μ—… λͺ¨λ“œ μ΄ˆκΈ°ν™”μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.'); + cleanup(); + } + }, [ + roomId, + userId, + userName, + enabled, + setConnectionStatus, + addUser, + clearUsers, + cleanup, + scheduleReconnect, + ]); + + useEffect(() => { + if (enabled && roomId && userId && userName) { + console.log(`[CodeRunner] μ΄ˆκΈ°ν™” 트리거:`, { roomId, userId, userName, enabled }); + initialize(); + } else { + console.log(`[CodeRunner] μ΄ˆκΈ°ν™” 쑰건 λ―ΈμΆ©μ‘±:`, { + enabled, + roomId: !!roomId, + userId: !!userId, + userName: !!userName, + }); + } + + return cleanup; + }, [enabled, roomId, userId, userName, initialize, cleanup]); + + return { + isConnected, + error, + commandHistory, + broadcastCommand, + }; +}; diff --git a/src/pages/Repo/RepoPage.tsx b/src/pages/Repo/RepoPage.tsx index 1773f738..2866b89a 100644 --- a/src/pages/Repo/RepoPage.tsx +++ b/src/pages/Repo/RepoPage.tsx @@ -359,7 +359,15 @@ export function RepoPage() {
- +
From 9e2112e8fd3adc385905d34f11756dd833b5aa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9E=AC=EA=B2=BD?= <82097725+Shin-Yu-1@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:27:38 +0900 Subject: [PATCH 17/30] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bd8289b..157bbf78 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Code: [FE](https://github.com/DeepDirect/deepwebide-fe), [BE](https://github.com | κΆŒν˜œμ§„ | Backend | [@sunsetkk](https://github.com/sunsetkk) | | 박건 | Frontend | [@Jammanb0](https://github.com/Jammanb0) | | λ°•μ†Œν˜„ | Frontend | [@ssoogit](https://github.com/ssoogit) | -| λ°•μž¬κ²½ | Full Stack, Infra | [@Shin-Yu-1](https://github.com/sunghoon-back) | +| λ°•μž¬κ²½ | Full Stack, Infra | [@Shin-Yu-1](https://github.com/Shin-Yu-1) | | 이은지 | Frontend | [@ebbll](https://github.com/ebbll) | | μ΅œλ²”κ·Ό | Backend | [@vayaconChoi](https://github.com/vayaconChoi) | From 7ef95da2091ca1843d6b042cbd882bf850ee15e7 Mon Sep 17 00:00:00 2001 From: Shin-Yu-1 Date: Sun, 3 Aug 2025 14:50:42 +0900 Subject: [PATCH 18/30] =?UTF-8?q?fix:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=ED=86=A0=EA=B8=80=20=EB=B3=80=EA=B2=BD=20=EB=95=8C=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=201=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A1=9C=20=EC=B4=88?= =?UTF-8?q?=EA=B8=B0=ED=99=94=20/=20DP-213?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx | 1 + src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx | 1 + src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx | 1 + 3 files changed, 3 insertions(+) diff --git a/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx b/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx index 1219c83a..7e4f4eb8 100644 --- a/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx +++ b/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx @@ -106,6 +106,7 @@ const PrivateRepoPage = () => { // μ’‹μ•„μš” ν•„ν„° const handleLikChange = () => { setIsLiked(!isLiked); + setPagination(prev => ({ ...prev, current: 1 })); repositoryRefetch(); }; diff --git a/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx b/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx index 4488eb58..755fe103 100644 --- a/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx +++ b/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx @@ -88,6 +88,7 @@ const SharedByMeRepoPage = () => { // μ’‹μ•„μš” ν•„ν„° const handleLikChange = () => { setIsLiked(!isLiked); + setPagination(prev => ({ ...prev, current: 1 })); repositoryRefetch(); }; diff --git a/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx b/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx index 42a2c00f..0f35d489 100644 --- a/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx +++ b/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx @@ -90,6 +90,7 @@ const SharedWithMeRepoPage = () => { // μ’‹μ•„μš” ν•„ν„° const handleLikChange = () => { setIsLiked(!isLiked); + setPagination(prev => ({ ...prev, current: 1 })); repositoryRefetch(); }; From 944dabd6e51978e90a47752ee29e7358dbea1c4c Mon Sep 17 00:00:00 2001 From: ssoogit Date: Sun, 3 Aug 2025 06:32:36 +0900 Subject: [PATCH 19/30] =?UTF-8?q?fix(editor):=20=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EB=A1=9C=EB=94=A9=20=EB=B0=A9=EC=A7=80=EC=9A=A9=20=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20/=20DP-204?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/repo/useFileContentLoader.ts | 72 ++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/src/hooks/repo/useFileContentLoader.ts b/src/hooks/repo/useFileContentLoader.ts index 2750b9bc..23ed388f 100644 --- a/src/hooks/repo/useFileContentLoader.ts +++ b/src/hooks/repo/useFileContentLoader.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import { useTabStore } from '@/stores/tabStore'; import { useFileContent } from './useFileContent'; @@ -17,6 +17,9 @@ export const useFileContentLoader = ({ }: UseFileContentLoaderParams) => { const { openTabs, setTabContentFromFile } = useTabStore(); + // 쀑볡 처리 λ°©μ§€λ₯Ό μœ„ν•œ ref + const processedTabsRef = useRef>(new Set()); + const activeTab = openTabs.find(tab => tab.isActive); console.log('FileContentLoader μƒνƒœ:', { @@ -27,19 +30,15 @@ export const useFileContentLoader = ({ shouldLoad: enabled && !enableCollaboration && activeTab && activeTab.content === '', }); - // λ‘œλ”© 쑰건: - // 1. enabledκ°€ true - // 2. ν˜‘μ—… λͺ¨λ“œκ°€ μ•„λ‹˜ (ν˜‘μ—… λͺ¨λ“œμ—μ„œλŠ” νŒŒμΌνŠΈλ¦¬μ—μ„œ 직접 λ‘œλ“œ) - // 3. ν™œμ„± 탭이 있음 - // 4. νƒ­ λ‚΄μš©μ΄ λΉ„μ–΄μžˆμŒ (아직 λ‘œλ“œλ˜μ§€ μ•ŠμŒ) - // 5. 탭이 ν˜„μž¬ 레포의 κ²ƒμž„ + // λ‘œλ”© 쑰건 + 쀑볡 처리 λ°©μ§€ const shouldLoadContent = enabled && !enableCollaboration && activeTab && activeTab.content === '' && activeTab.id.startsWith(`${repoId}/`) && - activeTab.fileId; + activeTab.fileId && + !processedTabsRef.current.has(activeTab.id); // 쀑볡 처리 λ°©μ§€ μΆ”κ°€ const { data: fileContentData, @@ -49,7 +48,7 @@ export const useFileContentLoader = ({ } = useFileContent({ repositoryId, fileId: activeTab?.fileId || 0, - enabled: Boolean(shouldLoadContent), // Boolean으둜 λ³€ν™˜ν•˜μ—¬ νƒ€μž… μ—λŸ¬ ν•΄κ²° + enabled: Boolean(shouldLoadContent), }); // 파일 λ‚΄μš© λ‘œλ“œ 성곡 μ‹œ 탭에 μ„€μ • @@ -58,6 +57,11 @@ export const useFileContentLoader = ({ const tabId = activeTab.id; const content = fileContentData.data.content; + // 이미 μ²˜λ¦¬ν•œ 탭인지 확인 + if (processedTabsRef.current.has(tabId)) { + return; + } + console.log('파일 λ‚΄μš© μ„€μ • (일반 λͺ¨λ“œ):', { tabId, filePath: activeTab.path, @@ -66,14 +70,33 @@ export const useFileContentLoader = ({ fileId: activeTab.fileId, }); + // 처리 μ™„λ£Œ ν‘œμ‹œ + processedTabsRef.current.add(tabId); + // setTabContentFromFile μ‚¬μš©μœΌλ‘œ clean μƒνƒœ 보μž₯ setTabContentFromFile(tabId, content); } - }, [fileContentData, activeTab, shouldLoadContent, setTabContentFromFile]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + fileContentData?.data?.content, + activeTab?.id, + activeTab?.path, + activeTab?.name, + activeTab?.fileId, + shouldLoadContent, + setTabContentFromFile, + ]); // μ—λŸ¬ 처리 useEffect(() => { if (error && activeTab && shouldLoadContent) { + const tabId = activeTab.id; + + // 이미 μ²˜λ¦¬ν•œ 탭인지 확인 + if (processedTabsRef.current.has(tabId)) { + return; + } + console.error('파일 λ‚΄μš© λ‘œλ“œ μ—λŸ¬:', { error, tabId: activeTab.id, @@ -93,9 +116,34 @@ export const useFileContentLoader = ({ // λ¬Έμ œκ°€ μ§€μ†λ˜λ©΄ νŽ˜μ΄μ§€λ₯Ό μƒˆλ‘œκ³ μΉ¨ν•΄μ£Όμ„Έμš”.`; + // 처리 μ™„λ£Œ ν‘œμ‹œ + processedTabsRef.current.add(tabId); setTabContentFromFile(activeTab.id, errorMessage); } - }, [error, activeTab, shouldLoadContent, setTabContentFromFile]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + error, + activeTab?.id, + activeTab?.path, + activeTab?.fileId, + shouldLoadContent, + setTabContentFromFile, + ]); + + // 탭이 λ³€κ²½λ˜λ©΄ 처리된 νƒ­ λͺ©λ‘ 정리 + useEffect(() => { + const currentTabIds = openTabs.map(tab => tab.id); + const newProcessedTabs = new Set(); + + // ν˜„μž¬ μ—΄λ¦° νƒ­λ“€λ§Œ μœ μ§€ + currentTabIds.forEach(tabId => { + if (processedTabsRef.current.has(tabId)) { + newProcessedTabs.add(tabId); + } + }); + + processedTabsRef.current = newProcessedTabs; + }, [openTabs]); // μˆ˜λ™ μƒˆλ‘œκ³ μΉ¨ ν•¨μˆ˜ const refreshCurrentFile = () => { @@ -105,6 +153,8 @@ export const useFileContentLoader = ({ filePath: activeTab.path, }); + // 처리 기둝 μ œκ±°ν•˜μ—¬ λ‹€μ‹œ λ‘œλ“œ κ°€λŠ₯ν•˜κ²Œ 함 + processedTabsRef.current.delete(activeTab.id); refetch(); } }; From fc21bba6434510394a3a7557e69f810c9caa3945 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 22:27:16 +0900 Subject: [PATCH 20/30] =?UTF-8?q?remove:=20=EB=8D=94=EC=9D=B4=EC=83=81=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=20=EC=97=86=EB=8A=94=20=EC=9B=B9=EC=86=8C?= =?UTF-8?q?=EC=BC=93=20=EB=B2=84=EC=A0=84=20=EC=B1=84=ED=8C=85=20=ED=8F=90?= =?UTF-8?q?=EA=B8=B0=20/=20DP-214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/chat/useWebSocketChat.ts | 270 ----------------------------- 1 file changed, 270 deletions(-) delete mode 100644 src/hooks/chat/useWebSocketChat.ts diff --git a/src/hooks/chat/useWebSocketChat.ts b/src/hooks/chat/useWebSocketChat.ts deleted file mode 100644 index dd5232f8..00000000 --- a/src/hooks/chat/useWebSocketChat.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { useEffect, useRef, useCallback, useState } from 'react'; -import { getWebSocketConfig, buildWebSocketUrl, wsLogger } from '@/utils/websocketConfig'; - -export interface ChatMessage { - id: string; - userId: string; - userName: string; - profileImageUrl: string; - content: string; - timestamp: number; - type: 'message' | 'system'; -} - -interface WebSocketMessage { - type: 'message' | 'join' | 'leave' | 'user_list' | 'message_history'; - data: - | ChatMessage - | ChatMessage[] - | { userId: string; userName: string } - | Record; - roomId?: string; - userId?: string; - token?: string; -} - -interface UseWebSocketChatProps { - roomId: string; - userId: string; - userName: string; - profileImageUrl: string; - enabled?: boolean; -} - -interface UseWebSocketChatReturn { - messages: ChatMessage[]; - sendMessage: (content: string) => void; - isConnected: boolean; - isLoading: boolean; - onlineUsers: Array<{ userId: string; userName: string }>; -} - -export const useWebSocketChat = ({ - roomId, - userId, - userName, - profileImageUrl, - enabled = true, -}: UseWebSocketChatProps): UseWebSocketChatReturn => { - const wsRef = useRef(null); - const reconnectTimeoutRef = useRef(null); - const isConnectingRef = useRef(false); - const [messages, setMessages] = useState([]); - const [isConnected, setIsConnected] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [onlineUsers, setOnlineUsers] = useState>([]); - - const cleanup = useCallback(() => { - wsLogger.info('WebSocket Chat μ—°κ²° 정리 쀑...'); - - if (reconnectTimeoutRef.current) { - clearTimeout(reconnectTimeoutRef.current); - reconnectTimeoutRef.current = null; - } - - if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) { - if (wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send( - JSON.stringify({ - type: 'leave', - data: { userId, userName }, - roomId, - userId, - }) - ); - } - wsRef.current.close(); - wsRef.current = null; - } - - setIsConnected(false); - setIsLoading(false); - isConnectingRef.current = false; - }, [userId, userName, roomId]); - - const sendWebSocketMessage = useCallback((message: WebSocketMessage) => { - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify(message)); - } - }, []); // dependency 제거 - wsRefλŠ” refμ΄λ―€λ‘œ μ•ˆμ •μ  - - const handleWebSocketMessage = useCallback((event: MessageEvent) => { - try { - const message: WebSocketMessage = JSON.parse(event.data); - - switch (message.type) { - case 'message': - if (message.data && typeof message.data === 'object' && 'id' in message.data) { - const chatMessage = message.data as ChatMessage; - setMessages(prev => { - if (prev.some(msg => msg.id === chatMessage.id)) { - return prev; - } - return [...prev, chatMessage].sort((a, b) => a.timestamp - b.timestamp); - }); - } - break; - - case 'message_history': - if (Array.isArray(message.data)) { - const historyMessages = message.data as ChatMessage[]; - setMessages(historyMessages.sort((a, b) => a.timestamp - b.timestamp)); - } - break; - - case 'user_list': - if (Array.isArray(message.data)) { - setOnlineUsers(message.data); - } - break; - - case 'join': - case 'leave': - wsLogger.info(`μ‚¬μš©μž ${message.type}:`, message.data); - break; - - default: - wsLogger.warn('μ•Œ 수 μ—†λŠ” λ©”μ‹œμ§€ νƒ€μž…:', message); - } - } catch (error) { - wsLogger.error('WebSocket λ©”μ‹œμ§€ νŒŒμ‹± 였λ₯˜:', error); - } - }, []); - - const connect = useCallback(() => { - if (!roomId || !enabled || isConnectingRef.current) { - return; - } - - // 이미 μ—°κ²°λ˜μ–΄ μžˆλ‹€λ©΄ μƒˆλ‘œ μ—°κ²°ν•˜μ§€ μ•ŠμŒ - if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { - return; - } - - isConnectingRef.current = true; - - // κΈ°μ‘΄ 연결이 μžˆλ‹€λ©΄ λ¨Όμ € 정리 - if (wsRef.current) { - if (wsRef.current.readyState === WebSocket.OPEN) { - wsRef.current.close(); - } - wsRef.current = null; - } - - try { - wsLogger.info('WebSocket Chat μ—°κ²° μ‹œμž‘:', roomId); - setIsLoading(true); - - const config = getWebSocketConfig(); - const token = sessionStorage.getItem('accessToken'); - - const wsUrl = buildWebSocketUrl(config.chatWsUrl, { - roomId, - userId, - userName: encodeURIComponent(userName), - ...(token && { token }), - }); - - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onopen = () => { - wsLogger.info('WebSocket Chat μ—°κ²° 성곡'); - setIsConnected(true); - setIsLoading(false); - isConnectingRef.current = false; - - // sendWebSocketMessage ν•¨μˆ˜λ₯Ό 직접 ν˜ΈμΆœν•˜μ§€ μ•Šκ³  인라인으둜 처리 - if (ws.readyState === WebSocket.OPEN) { - ws.send( - JSON.stringify({ - type: 'join', - data: { userId, userName }, - roomId, - userId, - token: token || undefined, - }) - ); - } - }; - - ws.onmessage = handleWebSocketMessage; - - ws.onclose = event => { - wsLogger.warn('WebSocket Chat μ—°κ²° μ’…λ£Œ:', event.code, event.reason); - setIsConnected(false); - setIsLoading(false); - wsRef.current = null; - isConnectingRef.current = false; - - // 정상 μ’…λ£Œκ°€ μ•„λ‹Œ 경우만 μž¬μ—°κ²° μ‹œλ„ - if (enabled && event.code !== 1000) { - const config = getWebSocketConfig(); - setTimeout(() => { - connect(); - }, config.reconnectOptions.delay); - } - }; - - ws.onerror = error => { - wsLogger.error('WebSocket Chat 였λ₯˜:', error); - setIsConnected(false); - setIsLoading(false); - isConnectingRef.current = false; - }; - } catch (error) { - wsLogger.error('WebSocket Chat μ—°κ²° μ‹€νŒ¨:', error); - setIsLoading(false); - isConnectingRef.current = false; - } - }, [roomId, enabled, userId, userName, handleWebSocketMessage]); // dependency μ΅œμ ν™” - - const sendMessage = useCallback( - (content: string) => { - if (!wsRef.current || !content.trim() || wsRef.current.readyState !== WebSocket.OPEN) { - return; - } - - const newMessage: ChatMessage = { - id: `${userId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - userId, - userName, - profileImageUrl, - content: content.trim(), - timestamp: Date.now(), - type: 'message', - }; - - sendWebSocketMessage({ - type: 'message', - data: newMessage, - roomId, - userId, - token: sessionStorage.getItem('accessToken') || undefined, - }); - }, - [userId, userName, profileImageUrl, roomId, sendWebSocketMessage] - ); - - // useRef둜 μ΅œμ‹  값듀을 μ•ˆμ •μ μœΌλ‘œ μ°Έμ‘° - const latestParamsRef = useRef({ enabled, roomId, userId, userName }); - useEffect(() => { - latestParamsRef.current = { enabled, roomId, userId, userName }; - }); - - useEffect(() => { - const { enabled, roomId, userId } = latestParamsRef.current; - if (enabled && roomId && userId) { - connect(); - } - return cleanup; - }, [enabled, roomId, userId, connect, cleanup]); - - return { - messages, - sendMessage, - isConnected, - isLoading, - onlineUsers, - }; -}; From 564ab3549a92cfdfd456cd5d4e061e21d3227524 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 22:27:29 +0900 Subject: [PATCH 21/30] =?UTF-8?q?remove:=20STOMP=20=EC=97=B0=EA=B2=B0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EC=9E=84=EC=8B=9C=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=9C=EA=B1=B0=20/?= =?UTF-8?q?=20DP-214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Chat/StompConnectionTest.tsx | 61 ----------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/features/Chat/StompConnectionTest.tsx diff --git a/src/features/Chat/StompConnectionTest.tsx b/src/features/Chat/StompConnectionTest.tsx deleted file mode 100644 index ecef5b0f..00000000 --- a/src/features/Chat/StompConnectionTest.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import useStompChat from '@/hooks/chat/useStompChat'; -import { useParams } from '@tanstack/react-router'; -import { useState } from 'react'; - -// const SOCKET_URL = `https://api.deepdirect.site/ws/chat/${token ? `?token=${token}` : ''}&repositoryId=${repositoryId}`; - -export const StompConnectionTest: React.FC = () => { - const [inputValue, setInputValue] = useState(''); - const { repoId } = useParams({ strict: false }); - - console.log('repoId from params:', repoId); - - const { isConnected, messages, send } = useStompChat( - 'https://api.deepdirect.site/ws/chat', - repoId - ); - - const handleSend = () => { - if (!inputValue.trim()) return; - - send({ - type: 'CHAT', - repositoryId: repoId, - message: inputValue, - codeReference: null, - }); - - setInputValue(''); - }; - - return ( -
-

STOMP Chat Room

-
- {messages.map((msg, index) => ( -
- {msg.repositoryId}: {msg.message} -
- ))} -
- - setInputValue(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') { - handleSend(); - } - }} - /> - - -
- ); -}; - -export default StompConnectionTest; From 7539954a582f35e8929ac6e61cff6d2ade230ee9 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 22:28:05 +0900 Subject: [PATCH 22/30] =?UTF-8?q?fix(chat):=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=201=EC=B4=88=20=EB=A1=9C=EB=94=A9=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20/=20DP-214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Chat/ChatStompVer.tsx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/features/Chat/ChatStompVer.tsx b/src/features/Chat/ChatStompVer.tsx index 2bbe8007..22ebba65 100644 --- a/src/features/Chat/ChatStompVer.tsx +++ b/src/features/Chat/ChatStompVer.tsx @@ -38,20 +38,11 @@ const Chat: React.FC = ({ isConnected, connectedCount, messages, const [totalMessages, setTotalMessages] = useState([]); const prevMessagesRef = useRef([]); const [searchResults, setSearchResults] = useState(null); - const [showLoading, setShowLoading] = useState(true); // ν˜„μž¬ μ‚¬μš©μž ID (λ©”μ‹œμ§€ λΉ„κ΅μš©) const currentUserId = getCurrentUserId(); const { data, fetchNextPage, hasNextPage, isSuccess } = useGetChatMessagesInfinite(repoId); - useEffect(() => { - const timer = setTimeout(() => { - setShowLoading(false); - }, 1000); - - return () => clearTimeout(timer); - }, []); - // SearchMessageData νƒ€μž…μ„ ChatReceivedMessage둜 λ³€ν™˜ const searchMessages: ChatReceivedMessage[] = searchResults ? searchResults.messages @@ -175,7 +166,7 @@ const Chat: React.FC = ({ isConnected, connectedCount, messages, {/* λ‘œλ”© 쀑일 λ•Œ λ‘œλ”© μ»΄ν¬λ„ŒνŠΈ ν‘œμ‹œ */} - {(!isConnected || showLoading) && } + {!isConnected && } {/* μ±„νŒ… λ©”μ‹œμ§€ λͺ©λ‘ */}
From 505d1626e48e39fe71109336013d6215e76acf22 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 22:30:36 +0900 Subject: [PATCH 23/30] =?UTF-8?q?feat(chat):=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=AA=85=20=EB=B3=80=EA=B2=BD=20/=20DP-214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Chat/{ChatStompVer.tsx => Chat.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/features/Chat/{ChatStompVer.tsx => Chat.tsx} (100%) diff --git a/src/features/Chat/ChatStompVer.tsx b/src/features/Chat/Chat.tsx similarity index 100% rename from src/features/Chat/ChatStompVer.tsx rename to src/features/Chat/Chat.tsx From 84c934d68ca623e27f3c042b01312c370d2b6d7e Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 22:30:48 +0900 Subject: [PATCH 24/30] =?UTF-8?q?fix(chat):=20=EC=B1=84=ED=8C=85=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9E=84=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95=20/=20DP-21?= =?UTF-8?q?4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/RepoLayout/RepoLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/RepoLayout/RepoLayout.tsx b/src/layouts/RepoLayout/RepoLayout.tsx index ced89a98..bfac9cd1 100644 --- a/src/layouts/RepoLayout/RepoLayout.tsx +++ b/src/layouts/RepoLayout/RepoLayout.tsx @@ -9,7 +9,7 @@ import { useThemeStore } from '@/stores/themeStore'; import { useFileSectionStore } from '@/stores/fileSectionStore'; import { useAuthStore } from '@/stores/authStore'; import { getCurrentUserId, getCurrentNickname } from '@/utils/authChatUtils'; -import Chat from '@/features/Chat/ChatStompVer'; +import Chat from '@/features/Chat/Chat'; import useStompChat from '@/hooks/chat/useStompChat'; export function RepoLayout() { From 7e7b5df3fe4f6345094aca98988edbe7b4c811f1 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 22:42:29 +0900 Subject: [PATCH 25/30] =?UTF-8?q?fix(chat):=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=A1=B0=EA=B1=B4=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20/=20DP-214?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/Chat/Chat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/Chat/Chat.tsx b/src/features/Chat/Chat.tsx index 22ebba65..07d4c961 100644 --- a/src/features/Chat/Chat.tsx +++ b/src/features/Chat/Chat.tsx @@ -72,7 +72,7 @@ const Chat: React.FC = ({ isConnected, connectedCount, messages, })) .sort((a, b) => new Date(a.sentAt).getTime() - new Date(b.sentAt).getTime()); setTotalMessages(formattedMessages); - if (!isSuccess) { + if (data.pageParams && data.pageParams.length === 1) { requestAnimationFrame(() => { scrollToBottom(); }); From 19f8c11e639ac63ff7b42172ca194974745e3e2f Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 23:09:46 +0900 Subject: [PATCH 26/30] =?UTF-8?q?fix(settings):=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20DEL?= =?UTF-8?q?ETE=20=EA=B8=80=EC=94=A8=20=EB=B9=A8=EA=B0=84=EC=83=89=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20/=20DP-215?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/SettingsPage/SettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/SettingsPage/SettingsPage.tsx b/src/pages/SettingsPage/SettingsPage.tsx index d9f4dcf9..7abc3847 100644 --- a/src/pages/SettingsPage/SettingsPage.tsx +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -123,7 +123,7 @@ const SettingsPage: React.FC = () => { onClick={() => scrollToSection('deleteSection')} > - DELETE + DELETE
)}
From 0c7fb22384be46c1e85dae31115cb8186027a1b3 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 23:10:12 +0900 Subject: [PATCH 27/30] =?UTF-8?q?style(settings):=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=84=B9=EC=85=98=EA=B0=84=20=EB=84=88?= =?UTF-8?q?=EB=B9=84=20=EB=8B=AC=EB=9E=90=EB=8D=98=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=ED=86=B5=EC=9D=BC=20/=20DP-215?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Settings/DeleteSection/DeleteSection.module.scss | 2 +- .../organisms/Settings/ShareSection/ShareSection.module.scss | 2 +- src/pages/SettingsPage/SettingsPage.module.scss | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/organisms/Settings/DeleteSection/DeleteSection.module.scss b/src/components/organisms/Settings/DeleteSection/DeleteSection.module.scss index 27abc9b6..2c97f10d 100644 --- a/src/components/organisms/Settings/DeleteSection/DeleteSection.module.scss +++ b/src/components/organisms/Settings/DeleteSection/DeleteSection.module.scss @@ -39,7 +39,7 @@ align-items: center; justify-content: space-between; gap: 0 2rem; - width: 883px; + width: 700px; height: 92px; font-family: $font-family-inter; transition-duration: 0.5s; diff --git a/src/components/organisms/Settings/ShareSection/ShareSection.module.scss b/src/components/organisms/Settings/ShareSection/ShareSection.module.scss index b8ebee5f..f775d293 100644 --- a/src/components/organisms/Settings/ShareSection/ShareSection.module.scss +++ b/src/components/organisms/Settings/ShareSection/ShareSection.module.scss @@ -64,7 +64,7 @@ flex-direction: row; align-items: center; justify-content: space-between; - width: 883px; + width: 700px; height: 92px; @include mobile { diff --git a/src/pages/SettingsPage/SettingsPage.module.scss b/src/pages/SettingsPage/SettingsPage.module.scss index d0b576bf..5cff5c92 100644 --- a/src/pages/SettingsPage/SettingsPage.module.scss +++ b/src/pages/SettingsPage/SettingsPage.module.scss @@ -24,6 +24,10 @@ } } +.deleteLabel { + color: $red; +} + .sharedByMeSettingsPage { display: flex; justify-content: flex-start; From 1d59b558e501292a8f7e4505be9b894fb3e9dca2 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 23:29:03 +0900 Subject: [PATCH 28/30] =?UTF-8?q?feat(repo):=20=ED=84=B0=EB=AF=B8=EB=84=90?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20=EB=B0=B0=EA=B2=BD=EC=83=89=20=EB=8B=A4?= =?UTF-8?q?=ED=81=AC=EB=AA=A8=EB=93=9C=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20/=20DP-216?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Repo/RepoPage.module.scss | 6 +++++- src/pages/Repo/RepoPage.tsx | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/Repo/RepoPage.module.scss b/src/pages/Repo/RepoPage.module.scss index bb7c2afd..db70126f 100644 --- a/src/pages/Repo/RepoPage.module.scss +++ b/src/pages/Repo/RepoPage.module.scss @@ -150,7 +150,11 @@ flex: 0 0 273px; box-sizing: border-box; overflow: hidden; - background-color: #343a40; + background-color: $gray-10; + + &.darkMode { + background-color: $gray-2; + } @media screen and (height <= 700px) { flex: 0 0 200px; diff --git a/src/pages/Repo/RepoPage.tsx b/src/pages/Repo/RepoPage.tsx index 2866b89a..70609b4f 100644 --- a/src/pages/Repo/RepoPage.tsx +++ b/src/pages/Repo/RepoPage.tsx @@ -9,6 +9,7 @@ import { useCollaborationStore } from '@/stores/collaborationStore'; import { useAuthStore } from '@/stores/authStore'; import { useFileContentLoader } from '@/hooks/repo/useFileContentLoader'; import { useYjsSavePoint } from '@/hooks/repo/useYjsSavePoint'; +import { useThemeStore } from '@/stores/themeStore'; import Loading from '@/components/molecules/Loading/Loading'; import styles from './RepoPage.module.scss'; import TabBar from '@/components/organisms/TabBar/TabBar'; @@ -38,6 +39,7 @@ export function RepoPage() { const search = useSearch({ strict: false }); const repoId = params.repoId; const filePath = search.file; + const { isDarkMode } = useThemeStore(); const { openTabs, @@ -358,7 +360,7 @@ export function RepoPage() {
-
+
Date: Sun, 3 Aug 2025 23:51:34 +0900 Subject: [PATCH 29/30] =?UTF-8?q?style(ShareSection):=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=ED=85=9C=20=EA=B0=84=20=EA=B0=84=EA=B2=A9,=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=EB=A7=81=ED=81=AC=20=EB=84=88=EB=B9=84=20=EC=A1=B0?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=98=B8?= =?UTF-8?q?=EB=B2=84=20=ED=9A=A8=EA=B3=BC=20=ED=86=B5=EC=9D=BC=20/=20DP-21?= =?UTF-8?q?7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Settings/ShareSection/ShareSection.module.scss | 13 ++++++++++--- .../Settings/ShareSection/ShareSection.tsx | 11 ++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/organisms/Settings/ShareSection/ShareSection.module.scss b/src/components/organisms/Settings/ShareSection/ShareSection.module.scss index f775d293..a46e4932 100644 --- a/src/components/organisms/Settings/ShareSection/ShareSection.module.scss +++ b/src/components/organisms/Settings/ShareSection/ShareSection.module.scss @@ -89,7 +89,9 @@ } .linkWrapper { - width: 424px; + display: flex; + flex-direction: row; + padding-right: 37px; @include mobile { width: 100%; @@ -110,7 +112,7 @@ .input { box-sizing: border-box; width: 100%; - max-width: 424px; + max-width: 350px; padding: 0.75rem 1rem; border: 1px solid $gray-8; border-radius: 6px; @@ -177,10 +179,15 @@ flex-direction: row; align-items: center; gap: 10px; - cursor: pointer; .icon { width: 20px; height: 20px; + cursor: pointer; + + &:hover { + opacity: 1; + transform: scale(1.1); + } } } diff --git a/src/components/organisms/Settings/ShareSection/ShareSection.tsx b/src/components/organisms/Settings/ShareSection/ShareSection.tsx index a13275ec..da2efa4d 100644 --- a/src/components/organisms/Settings/ShareSection/ShareSection.tsx +++ b/src/components/organisms/Settings/ShareSection/ShareSection.tsx @@ -113,6 +113,7 @@ const ShareSection = ({ onShareLinkCopy }: shareSectionProps) => {

SHARE

+ {/* 링크 곡유 */} {settingsData.isShared && (
@@ -127,14 +128,14 @@ const ShareSection = ({ onShareLinkCopy }: shareSectionProps) => {
- - {/* 곡유 링크 인풋 */} - {settingsData.shareLink && ( - - )} + {/* 곡유 링크 인풋 */} + {settingsData.shareLink && ( + + )} )} + {/* μž…μž₯ μ½”λ“œ */} {settingsData.isShared && isCurrentUserOwner(settingsData.members) && (
From 922926aef75d3bafbba9c4c69c2716342162a092 Mon Sep 17 00:00:00 2001 From: eunlee Date: Sun, 3 Aug 2025 23:56:00 +0900 Subject: [PATCH 30/30] =?UTF-8?q?style(InfoSection):=20=EC=95=84=EC=9D=B4?= =?UTF-8?q?=EC=BD=98=20=ED=98=B8=EB=B2=84=20=ED=9A=A8=EA=B3=BC=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20/=20DP-217?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../organisms/Settings/InfoSection/InfoSection.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/organisms/Settings/InfoSection/InfoSection.module.scss b/src/components/organisms/Settings/InfoSection/InfoSection.module.scss index 86942f7f..fe8a7c59 100644 --- a/src/components/organisms/Settings/InfoSection/InfoSection.module.scss +++ b/src/components/organisms/Settings/InfoSection/InfoSection.module.scss @@ -74,6 +74,12 @@ .icon { width: 20px; height: 20px; + cursor: pointer; + + &:hover { + opacity: 1; + transform: scale(1.1); + } } } }