From 880041c7c07a98a6d1881f854272ef95891de506 Mon Sep 17 00:00:00 2001 From: KIMB0B Date: Thu, 3 Sep 2026 17:04:02 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix(member):=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EC=8B=9C=20Integrity?= =?UTF-8?q?Error=20=EC=B2=98=EB=A6=AC=20=EC=A4=91=20AttributeError=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=EC=9D=B4=20=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/member/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/modules/member/service.py b/app/modules/member/service.py index 621ef77..773711d 100644 --- a/app/modules/member/service.py +++ b/app/modules/member/service.py @@ -256,7 +256,7 @@ async def update(self, target_member_id: UUID, actor: Member, data: MemberUpdate except Exception: logger.error(f"업로드 실패 후 이미지 롤백 삭제 실패: {profile_object_name}",exc_info=True) await self.session.rollback() #아까 했던 DB 작업 전부 취소 - raise AppError.bad_request(f"[{data.email}]은(는) 이미 존재하는 회원 이메일입니다.") + raise AppError.bad_request("요청하신 정보로 회원 정보를 수정할 수 없습니다.") async def update_role(self, member_id: UUID, role: MemberRole) -> Member: """ From 848402e1b2834455844ed90f18484f91d328d7d8 Mon Sep 17 00:00:00 2001 From: KIMB0B Date: Thu, 3 Sep 2026 17:05:40 +0900 Subject: [PATCH 2/6] =?UTF-8?q?fix(notice):=20hard=20delete=20=EC=8B=9C=20?= =?UTF-8?q?soft=20delete=20=EB=90=9C=20=EC=83=81=ED=83=9C=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=EC=B2=B4=ED=81=AC=20=EB=88=84=EB=9D=BD=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/notice/service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/modules/notice/service.py b/app/modules/notice/service.py index cf88394..d3e5c22 100644 --- a/app/modules/notice/service.py +++ b/app/modules/notice/service.py @@ -86,6 +86,8 @@ async def delete(self, target_notice_id: UUID, *, hard: bool = False) -> None: try: if hard: + if not notice.is_deleted: + raise AppError.bad_request("삭제되지 않은 공지입니다.") await self.repository.hard_delete(notice) else: if notice.is_deleted: From b40bbe9bb7e5b3f7fe829ac75a2ea8d05f9177a2 Mon Sep 17 00:00:00 2001 From: KIMB0B Date: Thu, 3 Sep 2026 17:28:36 +0900 Subject: [PATCH 3/6] =?UTF-8?q?fix(chat):=20DM=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=8B=9C=20=EB=8C=80=EC=83=81=20=EB=A9=A4=EB=B2=84=EC=9D=98=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=86=8C=EC=86=8D=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/chat/dependencies.py | 3 +- app/modules/chat/schemas.py | 1 - app/modules/chat/service.py | 7 +- app/static/member-detail.html | 126 +------------------------------ app/static/project-detail.html | 44 +++++++++-- 5 files changed, 50 insertions(+), 131 deletions(-) diff --git a/app/modules/chat/dependencies.py b/app/modules/chat/dependencies.py index 7644d8f..58057d7 100644 --- a/app/modules/chat/dependencies.py +++ b/app/modules/chat/dependencies.py @@ -3,6 +3,7 @@ from app.core.database import get_session from app.modules.chat.repository import ChatRepository from app.modules.chat.service import ChatService +from app.modules.project.repository import ProjectRepository async def get_chat_repository(session: AsyncSession = Depends(get_session)) -> ChatRepository: return ChatRepository(session) @@ -11,4 +12,4 @@ async def get_chat_service( session: AsyncSession = Depends(get_session), repository: ChatRepository = Depends(get_chat_repository) ) -> ChatService: - return ChatService(session, repository) + return ChatService(session, repository, ProjectRepository(session)) diff --git a/app/modules/chat/schemas.py b/app/modules/chat/schemas.py index 5d48f9d..a462644 100644 --- a/app/modules/chat/schemas.py +++ b/app/modules/chat/schemas.py @@ -29,7 +29,6 @@ class ChatRoomCreate(ChatRoomBase): pass class DirectChatRoomCreate(BaseModel): - project_id: UUID target_member_id: UUID class ChatRoomRead(ChatRoomBase): diff --git a/app/modules/chat/service.py b/app/modules/chat/service.py index 9778d7c..18cd6e5 100644 --- a/app/modules/chat/service.py +++ b/app/modules/chat/service.py @@ -9,6 +9,7 @@ from app.modules.chat.repository import ChatRepository from app.modules.chat.models import ChatRoom, ChatMessage from app.modules.chat.schemas import ChatMessageRead +from app.modules.project.repository import ProjectRepository from app.shared.enums import ChatRoomType from app.core.exceptions import AppError from app.core.redis import redis_client @@ -74,9 +75,10 @@ async def publish(self, room_id: UUID, message: dict): manager = ConnectionManager() class ChatService: - def __init__(self, session: AsyncSession, repository: ChatRepository): + def __init__(self, session: AsyncSession, repository: ChatRepository, project_repository: ProjectRepository): self.session = session self.repository = repository + self.project_repository = project_repository async def get_or_create_group_room(self, project_id: UUID) -> ChatRoom: """프로젝트의 단체 채팅방을 조회하거나 없으면 생성합니다.""" @@ -98,6 +100,9 @@ async def get_or_create_direct_room(self, project_id: UUID, member_a: UUID, memb if member_a == member_b: raise AppError.bad_request("자기 자신과는 대화할 수 없습니다.") + if not await self.project_repository.is_member(project_id, member_b): + raise AppError.bad_request("상대방이 해당 프로젝트의 멤버가 아닙니다.") + room = await self.repository.get_direct_room(project_id, member_a, member_b) if not room: room = ChatRoom( diff --git a/app/static/member-detail.html b/app/static/member-detail.html index 1cca35d..1310bbf 100644 --- a/app/static/member-detail.html +++ b/app/static/member-detail.html @@ -44,61 +44,6 @@ font-size: 1.1rem; margin-bottom: 2rem; } - .dm-section { - background: #f8f9fa; - border-radius: 12px; - padding: 1.5rem; - text-align: left; - border: 1px solid #e9ecef; - } - .dm-section label { - display: block; - font-weight: 700; - color: #495057; - margin-bottom: 0.8rem; - font-size: 0.9rem; - } - /* 커스텀 드롭다운 스타일 */ - .custom-select { - width: 100%; - padding: 0.8rem 1rem; - border: 2px solid #dee2e6; - border-radius: 8px; - font-size: 1rem; - background-color: #fff; - cursor: pointer; - transition: all 0.2s; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%236c757d' viewBox='0 0 16 16'%3E%3Cpath d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 1rem center; - } - .custom-select:focus { - border-color: #6f42c1; - outline: none; - box-shadow: 0 0 0 4px rgba(111, 66, 193, 0.1); - } - .btn-dm { - width: 100%; - padding: 1rem; - background: #6f42c1; - color: #fff; - border: none; - border-radius: 8px; - font-size: 1.1rem; - font-weight: 700; - cursor: pointer; - margin-top: 1.5rem; - transition: transform 0.2s, background 0.2s; - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - } - .btn-dm:hover { - background: #59359a; - transform: translateY(-2px); - } .btn-back { background: none; border: none; @@ -124,20 +69,10 @@

사용자

소속 정보 없음

-
- - -

- 💬 선택한 프로젝트 내에서 1:1 채팅방이 생성되거나 기존 대화로 이동합니다. -

-
+

+ 💬 DM은 함께 참여 중인 프로젝트 페이지의 멤버 목록에서 시작할 수 있습니다. +

- - @@ -155,7 +90,6 @@

사용자

} setupHeader(); await loadTargetUserInfo(); - await loadMyProjects(); }); function setupHeader() { @@ -193,60 +127,6 @@

사용자

} } - async function loadMyProjects() { - const token = localStorage.getItem('access_token'); - try { - const response = await fetch('/project/me', { - headers: { 'Authorization': `Bearer ${token}` } - }); - const result = await response.json(); - const projects = result.data.items; - - const select = document.getElementById('project-select'); - if (projects.length === 0) { - select.innerHTML = ''; - document.getElementById('btn-start-dm').disabled = true; - return; - } - - select.innerHTML = projects.map(p => ` - - `).join(''); - - } catch (e) { - console.error('프로젝트 목록 로드 실패', e); - } - } - - document.getElementById('btn-start-dm').addEventListener('click', async () => { - const token = localStorage.getItem('access_token'); - const projectId = document.getElementById('project-select').value; - - try { - const response = await fetch(`/chat/projects/${projectId}/rooms/direct`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - project_id: projectId, - target_member_id: targetMemberId - }) - }); - - const room = await response.json(); - if (response.ok) { - // 성공 시 해당 프로젝트 페이지의 채팅방으로 이동 - location.href = `/static/project-detail.html?id=${projectId}&chat_room_id=${room.id}`; - } else { - alert('채팅방 생성 실패: ' + (room.message || '오류가 발생했습니다.')); - } - } catch (e) { - alert('통신 오류 발생'); - } - }); - function logout() { localStorage.clear(); location.href = '/static/index.html'; diff --git a/app/static/project-detail.html b/app/static/project-detail.html index 00ea109..97fc4e5 100644 --- a/app/static/project-detail.html +++ b/app/static/project-detail.html @@ -609,15 +609,24 @@

${t.title}

const mResult = await mResponse.json(); const members = mResult.data; + const myId = localStorage.getItem('user_id'); const memberGrid = document.getElementById('proj-members-list'); memberGrid.innerHTML = members.map(m => ` -