Skip to content

Commit 5025e44

Browse files
authored
feat : 토론글 목록 조회 시 languageId 필드 추가 및 프론트 적용 (#160)
* feat : 토론글 목록 조회 시 languageId 필드 추가 * feat : 프론트 토론글 생성, 수정 시 언어 선택 기능 구현
1 parent 46cf80e commit 5025e44

File tree

5 files changed

+106
-19
lines changed

5 files changed

+106
-19
lines changed

src/main/java/org/ezcode/codetest/application/community/dto/response/DiscussionResponse.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public record DiscussionResponse(
2323
@Schema(description = "관련 문제 ID", example = "45", requiredMode = REQUIRED)
2424
Long problemId,
2525

26+
@Schema(description = "언어 ID", example = "1", requiredMode = REQUIRED)
27+
Long languageId,
28+
2629
@Schema(description = "토론 내용", example = "이 문제는 이렇게 풀 수 있습니다...", requiredMode = REQUIRED)
2730
String content,
2831

@@ -50,7 +53,8 @@ public static DiscussionResponse fromEntity(Discussion discussion) {
5053
return new DiscussionResponse(
5154
discussion.getId(),
5255
SimpleUserInfoResponse.fromEntity(discussion.getUser()),
53-
discussion.getProblem().getId(),
56+
discussion.getProblemId(),
57+
discussion.getLanguageId(),
5458
discussion.getContent(),
5559
discussion.getCreatedAt(),
5660
null,
@@ -66,6 +70,7 @@ public static DiscussionResponse from(DiscussionQueryResult result) {
6670
result.getDiscussionId(),
6771
result.getUserInfo(),
6872
result.getProblemId(),
73+
result.getLanguageId(),
6974
result.getContent(),
7075
result.getCreatedAt(),
7176
result.getUpvoteCount(),

src/main/java/org/ezcode/codetest/domain/community/dto/DiscussionQueryResult.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class DiscussionQueryResult {
1818

1919
private final Long problemId;
2020

21+
private final Long languageId;
22+
2123
private final String content;
2224

2325
private final LocalDateTime createdAt;
@@ -37,6 +39,7 @@ public DiscussionQueryResult(
3739
Long discussionId,
3840
SimpleUserInfoResponse userInfo,
3941
Long problemId,
42+
Long languageId,
4043
String content,
4144
LocalDateTime createdAt,
4245
Long upvoteCount,
@@ -48,6 +51,7 @@ public DiscussionQueryResult(
4851
this.discussionId = discussionId;
4952
this.userInfo = userInfo;
5053
this.problemId = problemId;
54+
this.languageId = languageId;
5155
this.content = content;
5256
this.createdAt = createdAt;
5357
this.upvoteCount = upvoteCount;

src/main/java/org/ezcode/codetest/domain/community/model/entity/Discussion.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,7 @@ public Long getProblemId() {
8181
return this.problem.getId();
8282
}
8383

84+
public Long getLanguageId() {
85+
return this.language.getId();
86+
}
8487
}

src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/community/discussion/DiscussionQueryRepositoryImpl.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ public List<Long> findDiscussionIdsByProblemId(Long problemId, String sortBy, Pa
4141

4242
NumberExpression<Long> bestScore = upvoteCount.subtract(downvoteCount);
4343

44-
long offset = pageable.getOffset();
45-
int pageNumber = pageable.getPageNumber();
46-
int pageSize = pageable.getPageSize();
47-
4844
return jpaQueryFactory
4945
.select(discussion.id)
5046
.from(discussion)
@@ -91,6 +87,7 @@ public List<DiscussionQueryResult> findDiscussionsByIds(List<Long> discussionIds
9187
user.profileImageUrl
9288
),
9389
discussion.problem.id,
90+
discussion.language.id,
9491
discussion.content,
9592
discussion.createdAt,
9693
upvoteCount,

src/main/resources/templates/test-submit.html

Lines changed: 92 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -372,16 +372,18 @@
372372
}
373373

374374
.discussion-item {
375-
background: #222;
376-
border-radius: 10px;
375+
background: #23272f;
376+
border-radius: 12px;
377377
margin-bottom: 18px;
378-
padding: 18px 16px;
379-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.10);
380-
transition: box-shadow 0.2s;
378+
padding: 22px 18px 18px 18px;
379+
box-shadow: 0 4px 18px rgba(0,0,0,0.18);
380+
border: 1.5px solid #2d323c;
381+
transition: box-shadow 0.2s, border 0.2s;
381382
}
382383

383384
.discussion-item:hover {
384-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
385+
box-shadow: 0 8px 28px rgba(0,0,0,0.28);
386+
border-color: #00d084;
385387
}
386388

387389
/* Header (profile/nickname) */
@@ -390,6 +392,7 @@
390392
align-items: center;
391393
gap: 8px;
392394
margin-bottom: 2px;
395+
margin-top: 8px;
393396
}
394397

395398
.discussion-header img {
@@ -779,6 +782,19 @@
779782
border-color: #00d084;
780783
font-weight: 700;
781784
}
785+
.lang-tag {
786+
display: inline-block;
787+
background: #2d323c;
788+
color: #00e09e;
789+
font-size: 13px;
790+
font-weight: 600;
791+
border-radius: 12px;
792+
padding: 4px 16px;
793+
margin-bottom: 14px;
794+
margin-top: 2px;
795+
margin-right: 0;
796+
letter-spacing: 0.5px;
797+
}
782798
</style>
783799
</head>
784800

@@ -800,6 +816,10 @@
800816
<li>메모리 제한: <span id="prob-mem">--</span> KB</li>
801817
<li>카테고리: <span id="prob-cats">--</span></li>
802818
</ul>
819+
820+
<div id="prob-image-wrapper">
821+
<img id="prob-image" src="" alt="문제 이미지" style="max-width: 100%; height: auto;" />
822+
</div>
803823
</div>
804824

805825
<div id="discussion-content" class="left-content">
@@ -816,10 +836,6 @@
816836
<div id="discussion-pagination"></div>
817837
</div>
818838

819-
<div id="prob-image-wrapper">
820-
<img id="prob-image" src="" alt="문제 이미지" style="max-width: 100%; height: auto;" />
821-
</div>
822-
823839
</div>
824840
<div class="right">
825841
<div class="section">
@@ -873,12 +889,48 @@ <h3>채점 결과</h3>
873889

874890
const DEFAULT_PROFILE_IMG = "/static/images/user-icon.png"; // 임의 프사 경로
875891

892+
// ====== 언어 목록 불러오기 및 전역 저장 ======
893+
async function loadLanguages() {
894+
try {
895+
const res = await fetch('/api/languages');
896+
const data = await res.json();
897+
if (data.success) {
898+
window.languageList = data.result;
899+
} else {
900+
window.languageList = [];
901+
}
902+
} catch {
903+
window.languageList = [];
904+
}
905+
}
906+
// 언어 드롭다운 HTML 생성
907+
function getLanguageSelectHtml(selectedId) {
908+
if (!window.languageList) return '';
909+
return `
910+
<select class="discussion-language-select">
911+
${window.languageList.map(lang =>
912+
`<option value="${lang.id}" ${lang.id === selectedId ? 'selected' : ''}>
913+
${lang.name} ${lang.version}
914+
</option>`
915+
).join('')}
916+
</select>
917+
`;
918+
}
919+
// 언어 id로 name+version 반환
920+
function getLanguageTagById(id) {
921+
if (!window.languageList) return '';
922+
const lang = window.languageList.find(l => l.id === id);
923+
return lang ? `${lang.name} ${lang.version}` : '';
924+
}
925+
876926
function tokenHeader() {
877927
const token = sessionStorage.getItem('accessToken');
878928
return token ? { 'Authorization': token.startsWith('Bearer ') ? token : 'Bearer ' + token } : {};
879929
}
880930

881-
window.addEventListener('DOMContentLoaded', () => {
931+
window.addEventListener('DOMContentLoaded', async () => {
932+
await loadLanguages();
933+
renderDiscussionCreateBox(); // 언어 목록 로드 후 호출
882934
const pid = new URLSearchParams(location.search).get('problemId');
883935
if (pid) {
884936
window.problemId = pid;
@@ -1180,6 +1232,7 @@ <h3>채점 결과</h3>
11801232
listEl.innerHTML = discussions.map(d => {
11811233
return `
11821234
<div class="discussion-item" data-discussion-id="${d.discussionId}">
1235+
<span class="lang-tag">${getLanguageTagById(d.languageId)}</span>
11831236
<div class="discussion-header">
11841237
<img src="${d.userInfo.profileImageUrl || DEFAULT_PROFILE_IMG}" alt="profile">
11851238
<span class="nickname">${d.userInfo.nickname}</span>
@@ -1409,10 +1462,22 @@ <h3>채점 결과</h3>
14091462
const oldContent = contentEl.textContent;
14101463
// 이미 수정창이 열려있으면 return
14111464
if (item.querySelector('.edit-input-box')) return;
1412-
// textarea + 취소/저장 버튼
1465+
// 기존 언어 id (discussion-item만)
1466+
let oldLangId = 1;
1467+
if (item.classList.contains('discussion-item')) {
1468+
// lang-tag에서 id 추출
1469+
const langTag = item.querySelector('.lang-tag');
1470+
if (langTag) {
1471+
const text = langTag.textContent.trim();
1472+
const found = window.languageList?.find(l => text.startsWith(l.name));
1473+
if (found) oldLangId = found.id;
1474+
}
1475+
}
1476+
// textarea + 언어 드롭다운 + 취소/저장 버튼
14131477
const box = document.createElement('div');
14141478
box.className = 'edit-input-box';
14151479
box.innerHTML = `
1480+
<div style="margin-bottom:8px;">${item.classList.contains('discussion-item') ? getLanguageSelectHtml(oldLangId) : ''}</div>
14161481
<textarea class="edit-input-text" style="width:100%;min-height:60px;">${oldContent}</textarea>
14171482
<div style="display:flex;gap:8px;justify-content:flex-end;">
14181483
<button class="edit-cancel-btn">취소</button>
@@ -1431,13 +1496,19 @@ <h3>채점 결과</h3>
14311496
// 저장
14321497
box.querySelector('.edit-save-btn').onclick = async () => {
14331498
const newContent = box.querySelector('.edit-input-text').value.trim();
1499+
let newLangId = oldLangId;
1500+
if (item.classList.contains('discussion-item')) {
1501+
newLangId = +box.querySelector('.discussion-language-select').value;
1502+
}
14341503
if (!newContent) return alert('내용을 입력하세요.');
1435-
let url, method = 'PUT', payload = { languageId: 1, content: newContent };
1504+
let url, method = 'PUT', payload = { languageId: newLangId, content: newContent };
14361505
if (item.classList.contains('discussion-item')) {
14371506
url = `/api/problems/${window.problemId}/discussions/${e.target.dataset.id}`;
14381507
} else {
14391508
const discussionId = item.closest('.discussion-item').dataset.discussionId;
14401509
url = `/api/problems/${window.problemId}/discussions/${discussionId}/replies/${e.target.dataset.id}`;
1510+
// 대댓글/댓글은 언어 변경 불가 (기존대로 1)
1511+
payload.languageId = 1;
14411512
}
14421513
const res = await fetch(url, {
14431514
method,
@@ -1659,7 +1730,13 @@ <h3>채점 결과</h3>
16591730

16601731
function renderDiscussionCreateBox() {
16611732
const box = document.getElementById('discussion-create-box');
1733+
if (!window.languageList || window.languageList.length === 0) {
1734+
box.innerHTML = '<div style="color:#aaa;">언어 목록을 불러오는 중...</div>';
1735+
return;
1736+
}
1737+
// 언어 드롭다운 + textarea
16621738
box.innerHTML = `
1739+
<div style="margin-bottom:8px;">${getLanguageSelectHtml(window.languageList?.[0]?.id || 1)}</div>
16631740
<textarea id="discussion-create-text" class="reply-input-text" rows="4" placeholder="토론글을 작성해보세요"></textarea>
16641741
<div style="display:flex;gap:10px;justify-content:flex-end;">
16651742
<button id="discussion-create-cancel" class="reply-cancel-btn">취소</button>
@@ -1673,6 +1750,7 @@ <h3>채점 결과</h3>
16731750
// 작성
16741751
box.querySelector('#discussion-create-submit').onclick = async () => {
16751752
const content = box.querySelector('#discussion-create-text').value.trim();
1753+
const langId = +box.querySelector('.discussion-language-select').value;
16761754
if (!content) return alert('내용을 입력하세요.');
16771755
const token = sessionStorage.getItem('accessToken');
16781756
const res = await fetch(`/api/problems/${window.problemId}/discussions`, {
@@ -1681,7 +1759,7 @@ <h3>채점 결과</h3>
16811759
'Content-Type': 'application/json',
16821760
...tokenHeader()
16831761
},
1684-
body: JSON.stringify({ languageId: 1, content })
1762+
body: JSON.stringify({ languageId: langId, content })
16851763
});
16861764
if (res.ok) {
16871765
// 성공: 입력창 비우고 목록 새로고침

0 commit comments

Comments
 (0)