Skip to content

Commit 6274dd2

Browse files
Merge pull request #52 from pirogramming/feat-jh
Feat jh
2 parents c6edcb7 + 345925a commit 6274dd2

5 files changed

Lines changed: 307 additions & 13 deletions

File tree

backend-chat/app/services/chat_service.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,15 @@ async def _redis_sub_listener(self, room_id):
6565
"content": "미션이 완료되었습니다."
6666
}, room_id)
6767
else:
68-
message = payload
69-
if msg_type == "SYSTEM" and "content" not in payload:
68+
if msg_type == "SYSTEM":
7069
content = ""
7170
if isinstance(data, dict):
7271
content = data.get("content", "")
7372
else:
7473
content = str(data)
75-
message = {
76-
"type": "SYSTEM",
77-
"content": content
78-
}
74+
message = {"type": "SYSTEM", "content": content, **data}
75+
else:
76+
message = payload
7977
await self._local_broadcast(message, room_id)
8078
except Exception as e:
8179
print(f"FastAPI Redis 리스너 에러: {e}")

backend-core/missions/views.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,11 @@ def mission_accept(request, mission_id):
282282
publish_chat_event(
283283
room_id=rid,
284284
event_type="SYSTEM",
285-
data={"content": f"{requester_name}님이 미션 수락을 요청했습니다. 등록자가 확정하면 매칭이 완료됩니다."}
285+
data={
286+
"content": f"{requester_name}님이 미션 수락을 요청했습니다. 등록자가 확정하면 매칭이 완료됩니다.",
287+
"mission_status": "PENDING_APPROVAL",
288+
"action": "mission_accepted",
289+
},
286290
)
287291

288292
return JsonResponse({
@@ -483,6 +487,8 @@ def chat_room(request: HttpRequest, mission_id: int, room_id: int) -> HttpRespon
483487
mission = room.mission
484488
is_author = request.user == mission.author
485489
can_accept = not is_author and mission.status == "WAITING"
490+
can_confirm_performer = is_author and mission.status == "PENDING_APPROVAL"
491+
show_complete_btn = is_author and mission.status == "MATCHED"
486492
blockable_user = {"id": other_user.id, "username": other_user.username} if other_user else None
487493

488494
return render(
@@ -495,6 +501,8 @@ def chat_room(request: HttpRequest, mission_id: int, room_id: int) -> HttpRespon
495501
"mission_id": mission.id,
496502
"is_author": is_author,
497503
"can_accept": can_accept,
504+
"can_confirm_performer": can_confirm_performer,
505+
"show_complete_btn": show_complete_btn,
498506
"blockable_user": blockable_user,
499507
"other_user": other_user,
500508
},

backend-core/static/chat/js/room.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ class ChatClient {
5555
});
5656
}
5757

58+
// 미션 완료 버튼 (등록자)
59+
const completeMissionBtn = document.getElementById('completeMissionBtn');
60+
if (completeMissionBtn) {
61+
completeMissionBtn.addEventListener('click', () => this.completeMission());
62+
}
63+
64+
// 수행자 확정 버튼 (등록자)
65+
const confirmPerformerBtn = document.getElementById('confirmPerformerBtn');
66+
if (confirmPerformerBtn) {
67+
confirmPerformerBtn.addEventListener('click', () => this.confirmPerformer());
68+
}
69+
// 수행자 거부 버튼 (등록자)
70+
const rejectPerformerBtn = document.getElementById('rejectPerformerBtn');
71+
if (rejectPerformerBtn) {
72+
rejectPerformerBtn.addEventListener('click', () => this.rejectPerformer());
73+
}
74+
5875
const moreMenuBtn = document.getElementById('moreMenuBtn');
5976
const moreDropdown = document.getElementById('moreDropdown');
6077
if (moreMenuBtn && moreDropdown) {
@@ -134,6 +151,106 @@ class ChatClient {
134151
}
135152
}
136153

154+
async confirmPerformer() {
155+
const btn = document.getElementById('confirmPerformerBtn');
156+
if (!btn) return;
157+
btn.disabled = true;
158+
try {
159+
const res = await fetch(`/api/missions/api/${this.missionId}/confirm/`, {
160+
method: 'POST',
161+
credentials: 'include',
162+
headers: this.getAuthHeaders(),
163+
body: JSON.stringify({ room_id: this.roomId }),
164+
});
165+
const data = await res.json().catch(() => ({}));
166+
if (res.ok && data.success) {
167+
alert(data.message || '수행자가 확정되었습니다.');
168+
169+
// 수행자 확정/거부 버튼 제거
170+
if (btn) btn.remove();
171+
const rejectBtn = document.getElementById('rejectPerformerBtn');
172+
if (rejectBtn) rejectBtn.remove();
173+
174+
const chatActions = document.getElementById('chatActions');
175+
if (chatActions && !document.getElementById('completeMissionBtn')) {
176+
const completeBtn = document.createElement('button');
177+
completeBtn.type = 'button';
178+
completeBtn.className = 'btn-action-gray';
179+
completeBtn.id = 'completeMissionBtn';
180+
completeBtn.textContent = '미션 완료';
181+
// 클릭 이벤트 연결
182+
completeBtn.addEventListener('click', () => this.completeMission());
183+
chatActions.appendChild(completeBtn);
184+
}
185+
} else {
186+
alert(data.error || '확정에 실패했습니다.');
187+
btn.disabled = false;
188+
}
189+
} catch (err) {
190+
console.error(err);
191+
alert('요청 중 오류가 발생했습니다.');
192+
btn.disabled = false;
193+
}
194+
}
195+
196+
async rejectPerformer() {
197+
const btn = document.getElementById('rejectPerformerBtn');
198+
if (!btn) return;
199+
if (!confirm('수행자 수락을 거절하시겠습니까?')) return;
200+
btn.disabled = true;
201+
try {
202+
const res = await fetch(`/api/missions/api/${this.missionId}/reject/`, {
203+
method: 'POST',
204+
credentials: 'include',
205+
headers: this.getAuthHeaders(),
206+
body: JSON.stringify({ room_id: this.roomId }),
207+
});
208+
const data = await res.json().catch(() => ({}));
209+
if (res.ok && data.success) {
210+
alert(data.message || '수락을 거절했습니다.');
211+
if (btn) btn.remove();
212+
const confirmBtn = document.getElementById('confirmPerformerBtn');
213+
if (confirmBtn) confirmBtn.remove();
214+
} else {
215+
alert(data.error || '거절에 실패했습니다.');
216+
btn.disabled = false;
217+
}
218+
} catch (err) {
219+
console.error(err);
220+
alert('요청 중 오류가 발생했습니다.');
221+
btn.disabled = false;
222+
}
223+
}
224+
225+
async completeMission() {
226+
const btn = document.getElementById('completeMissionBtn');
227+
if (!btn) return;
228+
if (!confirm('미션을 완료 처리하시겠습니까?')) return;
229+
btn.disabled = true;
230+
try {
231+
const res = await fetch(`/api/missions/api/${this.missionId}/complete/`, {
232+
method: 'POST',
233+
credentials: 'include',
234+
headers: this.getAuthHeaders(),
235+
body: JSON.stringify({ room_id: this.roomId }),
236+
});
237+
const data = await res.json().catch(() => ({}));
238+
if (res.ok && data.success) {
239+
alert(data.message || '미션이 완료되었습니다.');
240+
if (btn) btn.remove();
241+
// 원하면 여기서 채팅 목록 등 다른 페이지로 이동
242+
// window.location.href = '/api/missions/chat/';
243+
} else {
244+
alert(data.error || '미션 완료에 실패했습니다.');
245+
btn.disabled = false;
246+
}
247+
} catch (err) {
248+
console.error(err);
249+
alert('요청 중 오류가 발생했습니다.');
250+
btn.disabled = false;
251+
}
252+
}
253+
137254
async blockUser(targetId, username, btnEl) {
138255
if (!btnEl) return;
139256
if (!confirm(`${username || '해당 유저'}를 차단하시겠습니까?`)) return;
@@ -285,6 +402,28 @@ class ChatClient {
285402
};
286403
}
287404

405+
showConfirmButtons() {
406+
const chatActions = document.getElementById('chatActions');
407+
if (!chatActions) return;
408+
if (document.getElementById('confirmPerformerBtn')) return;
409+
410+
const confirmBtn = document.createElement('button');
411+
confirmBtn.type = 'button';
412+
confirmBtn.className = 'btn-action-outline';
413+
confirmBtn.id = 'confirmPerformerBtn';
414+
confirmBtn.innerHTML = '<i class="fa-regular fa-circle-check"></i> 수행자 확정';
415+
confirmBtn.addEventListener('click', () => this.confirmPerformer());
416+
chatActions.appendChild(confirmBtn);
417+
418+
const rejectBtn = document.createElement('button');
419+
rejectBtn.type = 'button';
420+
rejectBtn.className = 'btn-action-gray';
421+
rejectBtn.id = 'rejectPerformerBtn';
422+
rejectBtn.textContent = '수행자 거부';
423+
rejectBtn.addEventListener('click', () => this.rejectPerformer());
424+
chatActions.appendChild(rejectBtn);
425+
}
426+
288427
handleMessage(msg) {
289428
switch (msg.type) {
290429
case 'AUTH_SUCCESS':
@@ -305,6 +444,9 @@ class ChatClient {
305444

306445
case 'SYSTEM':
307446
this.displaySystemMessage(msg.content);
447+
if (msg.action === 'mission_accepted' && this.isAuthor) {
448+
this.showConfirmButtons();
449+
}
308450
break;
309451

310452
case 'KICK':

backend-core/staticfiles/chat/js/room.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ class ChatClient {
5555
});
5656
}
5757

58+
// 미션 완료 버튼 (등록자)
59+
const completeMissionBtn = document.getElementById('completeMissionBtn');
60+
if (completeMissionBtn) {
61+
completeMissionBtn.addEventListener('click', () => this.completeMission());
62+
}
63+
64+
// 수행자 확정 버튼 (등록자)
65+
const confirmPerformerBtn = document.getElementById('confirmPerformerBtn');
66+
if (confirmPerformerBtn) {
67+
confirmPerformerBtn.addEventListener('click', () => this.confirmPerformer());
68+
}
69+
// 수행자 거부 버튼 (등록자)
70+
const rejectPerformerBtn = document.getElementById('rejectPerformerBtn');
71+
if (rejectPerformerBtn) {
72+
rejectPerformerBtn.addEventListener('click', () => this.rejectPerformer());
73+
}
74+
5875
const moreMenuBtn = document.getElementById('moreMenuBtn');
5976
const moreDropdown = document.getElementById('moreDropdown');
6077
if (moreMenuBtn && moreDropdown) {
@@ -134,6 +151,106 @@ class ChatClient {
134151
}
135152
}
136153

154+
async confirmPerformer() {
155+
const btn = document.getElementById('confirmPerformerBtn');
156+
if (!btn) return;
157+
btn.disabled = true;
158+
try {
159+
const res = await fetch(`/api/missions/api/${this.missionId}/confirm/`, {
160+
method: 'POST',
161+
credentials: 'include',
162+
headers: this.getAuthHeaders(),
163+
body: JSON.stringify({ room_id: this.roomId }),
164+
});
165+
const data = await res.json().catch(() => ({}));
166+
if (res.ok && data.success) {
167+
alert(data.message || '수행자가 확정되었습니다.');
168+
169+
// 수행자 확정/거부 버튼 제거
170+
if (btn) btn.remove();
171+
const rejectBtn = document.getElementById('rejectPerformerBtn');
172+
if (rejectBtn) rejectBtn.remove();
173+
174+
const chatActions = document.getElementById('chatActions');
175+
if (chatActions && !document.getElementById('completeMissionBtn')) {
176+
const completeBtn = document.createElement('button');
177+
completeBtn.type = 'button';
178+
completeBtn.className = 'btn-action-gray';
179+
completeBtn.id = 'completeMissionBtn';
180+
completeBtn.textContent = '미션 완료';
181+
// 클릭 이벤트 연결
182+
completeBtn.addEventListener('click', () => this.completeMission());
183+
chatActions.appendChild(completeBtn);
184+
}
185+
} else {
186+
alert(data.error || '확정에 실패했습니다.');
187+
btn.disabled = false;
188+
}
189+
} catch (err) {
190+
console.error(err);
191+
alert('요청 중 오류가 발생했습니다.');
192+
btn.disabled = false;
193+
}
194+
}
195+
196+
async rejectPerformer() {
197+
const btn = document.getElementById('rejectPerformerBtn');
198+
if (!btn) return;
199+
if (!confirm('수행자 수락을 거절하시겠습니까?')) return;
200+
btn.disabled = true;
201+
try {
202+
const res = await fetch(`/api/missions/api/${this.missionId}/reject/`, {
203+
method: 'POST',
204+
credentials: 'include',
205+
headers: this.getAuthHeaders(),
206+
body: JSON.stringify({ room_id: this.roomId }),
207+
});
208+
const data = await res.json().catch(() => ({}));
209+
if (res.ok && data.success) {
210+
alert(data.message || '수락을 거절했습니다.');
211+
if (btn) btn.remove();
212+
const confirmBtn = document.getElementById('confirmPerformerBtn');
213+
if (confirmBtn) confirmBtn.remove();
214+
} else {
215+
alert(data.error || '거절에 실패했습니다.');
216+
btn.disabled = false;
217+
}
218+
} catch (err) {
219+
console.error(err);
220+
alert('요청 중 오류가 발생했습니다.');
221+
btn.disabled = false;
222+
}
223+
}
224+
225+
async completeMission() {
226+
const btn = document.getElementById('completeMissionBtn');
227+
if (!btn) return;
228+
if (!confirm('미션을 완료 처리하시겠습니까?')) return;
229+
btn.disabled = true;
230+
try {
231+
const res = await fetch(`/api/missions/api/${this.missionId}/complete/`, {
232+
method: 'POST',
233+
credentials: 'include',
234+
headers: this.getAuthHeaders(),
235+
body: JSON.stringify({ room_id: this.roomId }),
236+
});
237+
const data = await res.json().catch(() => ({}));
238+
if (res.ok && data.success) {
239+
alert(data.message || '미션이 완료되었습니다.');
240+
if (btn) btn.remove();
241+
// 원하면 여기서 채팅 목록 등 다른 페이지로 이동
242+
// window.location.href = '/api/missions/chat/';
243+
} else {
244+
alert(data.error || '미션 완료에 실패했습니다.');
245+
btn.disabled = false;
246+
}
247+
} catch (err) {
248+
console.error(err);
249+
alert('요청 중 오류가 발생했습니다.');
250+
btn.disabled = false;
251+
}
252+
}
253+
137254
async blockUser(targetId, username, btnEl) {
138255
if (!btnEl) return;
139256
if (!confirm(`${username || '해당 유저'}를 차단하시겠습니까?`)) return;
@@ -285,6 +402,28 @@ class ChatClient {
285402
};
286403
}
287404

405+
showConfirmButtons() {
406+
const chatActions = document.getElementById('chatActions');
407+
if (!chatActions) return;
408+
if (document.getElementById('confirmPerformerBtn')) return;
409+
410+
const confirmBtn = document.createElement('button');
411+
confirmBtn.type = 'button';
412+
confirmBtn.className = 'btn-action-outline';
413+
confirmBtn.id = 'confirmPerformerBtn';
414+
confirmBtn.innerHTML = '<i class="fa-regular fa-circle-check"></i> 수행자 확정';
415+
confirmBtn.addEventListener('click', () => this.confirmPerformer());
416+
chatActions.appendChild(confirmBtn);
417+
418+
const rejectBtn = document.createElement('button');
419+
rejectBtn.type = 'button';
420+
rejectBtn.className = 'btn-action-gray';
421+
rejectBtn.id = 'rejectPerformerBtn';
422+
rejectBtn.textContent = '수행자 거부';
423+
rejectBtn.addEventListener('click', () => this.rejectPerformer());
424+
chatActions.appendChild(rejectBtn);
425+
}
426+
288427
handleMessage(msg) {
289428
switch (msg.type) {
290429
case 'AUTH_SUCCESS':
@@ -305,6 +444,9 @@ class ChatClient {
305444

306445
case 'SYSTEM':
307446
this.displaySystemMessage(msg.content);
447+
if (msg.action === 'mission_accepted' && this.isAuthor) {
448+
this.showConfirmButtons();
449+
}
308450
break;
309451

310452
case 'KICK':

0 commit comments

Comments
 (0)