-
+
대상 공지 발송
- 닉네임 검색 결과에서 사용자를 선택하세요. 중복 선택은 자동으로 제외됩니다.
+ 닉네임으로 검색해 결과를 클릭하거나, ID(숫자)를 직접 입력해 추가할 수 있습니다.
+ 콤마/공백으로 구분해 여러 ID를 한 번에 추가하거나 Enter로 첫 번째 항목을 선택하세요.
@@ -159,12 +160,42 @@
미리보기
if (!user) {
return '';
}
- if (user.nickname && user.nickname.charAt(0) === '#') {
+ if (!user.nickname) {
+ return '#' + user.id;
+ }
+ if (user.nickname.charAt(0) === '#') {
return user.nickname;
}
return user.nickname + ' (#' + user.id + ')';
}
+ function parseNumericTokens(raw) {
+ if (!raw) {
+ return null;
+ }
+ var tokens = raw.trim().split(/[,\s]+/).filter(Boolean);
+ if (tokens.length === 0) {
+ return null;
+ }
+ var seen = {};
+ var ids = [];
+ for (var i = 0; i < tokens.length; i++) {
+ if (!/^\d+$/.test(tokens[i])) {
+ return null;
+ }
+ var n = parseInt(tokens[i], 10);
+ if (!(n > 0)) {
+ return null;
+ }
+ var idStr = String(n);
+ if (!seen[idStr]) {
+ seen[idStr] = true;
+ ids.push(idStr);
+ }
+ }
+ return ids;
+ }
+
function syncSelectedRecipients() {
if (userIdsEl) {
userIdsEl.value = selectedUsers.map(function (user) {
@@ -243,7 +274,71 @@
미리보기
syncPreview();
}
- function renderSuggestions(users) {
+ function addRecipientsByIds(ids) {
+ ids.forEach(function (rawId) {
+ var id = String(rawId);
+ var exists = selectedUsers.some(function (selected) {
+ return selected.id === id;
+ });
+ if (!exists) {
+ selectedUsers.push({ id: id, nickname: '#' + id });
+ }
+ });
+ if (searchEl) {
+ searchEl.value = '';
+ searchEl.focus();
+ }
+ hideSuggestions();
+ syncSelectedRecipients();
+ syncPreview();
+ }
+
+ function buildSuggestionButton(leftText, rightText, onClick) {
+ var button = document.createElement('button');
+ button.type = 'button';
+ button.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
+ var leftSpan = document.createElement('span');
+ leftSpan.textContent = leftText;
+ var rightSpan = document.createElement('span');
+ rightSpan.className = 'text-muted small';
+ rightSpan.textContent = rightText;
+ button.appendChild(leftSpan);
+ button.appendChild(rightSpan);
+ button.addEventListener('click', onClick);
+ return button;
+ }
+
+ function renderBulkIdSuggestions(ids) {
+ if (!suggestionsEl) {
+ return;
+ }
+ suggestionsEl.innerHTML = '';
+ var unselected = ids.filter(function (id) {
+ return !selectedUsers.some(function (selected) {
+ return selected.id === id;
+ });
+ });
+ if (unselected.length === 0) {
+ showSuggestionMessage('입력한 ID가 모두 이미 추가되어 있습니다.');
+ return;
+ }
+ var preview = unselected.slice(0, 5).map(function (id) {
+ return '#' + id;
+ }).join(', ');
+ if (unselected.length > 5) {
+ preview += ' 외 ' + (unselected.length - 5) + '개';
+ }
+ suggestionsEl.appendChild(buildSuggestionButton(
+ unselected.length + '개 ID 일괄 추가',
+ preview,
+ function () {
+ addRecipientsByIds(unselected);
+ }
+ ));
+ suggestionsEl.classList.remove('d-none');
+ }
+
+ function renderSuggestions(users, directAddId) {
if (!suggestionsEl) {
return;
}
@@ -253,22 +348,32 @@
미리보기
return selected.id === String(user.id);
});
});
- if (candidates.length === 0) {
- showSuggestionMessage('검색 결과가 없습니다.');
- return;
+ var needsDirectAdd = directAddId
+ && !selectedUsers.some(function (s) { return s.id === directAddId; })
+ && !candidates.some(function (u) { return String(u.id) === directAddId; });
+ if (needsDirectAdd) {
+ suggestionsEl.appendChild(buildSuggestionButton(
+ 'ID 그대로 추가',
+ '#' + directAddId,
+ function () {
+ addRecipientsByIds([directAddId]);
+ }
+ ));
}
candidates.forEach(function (user) {
- var button = document.createElement('button');
- button.type = 'button';
- button.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
- button.innerHTML = '
';
- button.querySelector('span:first-child').textContent = user.nickname;
- button.querySelector('span:last-child').textContent = '#' + user.id;
- button.addEventListener('click', function () {
- addRecipient(user);
- });
- suggestionsEl.appendChild(button);
+ var label = user.nickname || '(닉네임 없음)';
+ suggestionsEl.appendChild(buildSuggestionButton(
+ label,
+ '#' + user.id,
+ function () {
+ addRecipient(user);
+ }
+ ));
});
+ if (!suggestionsEl.firstChild) {
+ showSuggestionMessage('검색 결과가 없습니다.');
+ return;
+ }
suggestionsEl.classList.remove('d-none');
}
@@ -278,6 +383,13 @@
미리보기
hideSuggestions();
return;
}
+ var numericIds = parseNumericTokens(trimmed);
+ if (numericIds && numericIds.length > 1) {
+ latestSearchSeq++;
+ renderBulkIdSuggestions(numericIds);
+ return;
+ }
+ var directAddId = numericIds && numericIds.length === 1 ? numericIds[0] : null;
var searchSeq = ++latestSearchSeq;
fetch('/notifications/users/search?q=' + encodeURIComponent(trimmed), {
headers: {
@@ -295,10 +407,15 @@
미리보기
if (searchSeq !== latestSearchSeq) {
return;
}
- renderSuggestions(data && data.users ? data.users : []);
+ renderSuggestions(data && data.users ? data.users : [], directAddId);
})
.catch(function () {
- if (searchSeq === latestSearchSeq) {
+ if (searchSeq !== latestSearchSeq) {
+ return;
+ }
+ if (directAddId) {
+ renderSuggestions([], directAddId);
+ } else {
showSuggestionMessage('사용자 검색에 실패했습니다.');
}
});
@@ -345,6 +462,20 @@
미리보기
searchEl.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
hideSuggestions();
+ return;
+ }
+ if (event.key === 'Enter') {
+ if (suggestionsEl && !suggestionsEl.classList.contains('d-none')) {
+ var firstAction = suggestionsEl.querySelector('button.list-group-item-action');
+ if (firstAction) {
+ event.preventDefault();
+ firstAction.click();
+ return;
+ }
+ }
+ if (searchEl.value.trim()) {
+ event.preventDefault();
+ }
}
});
}
diff --git a/src/main/resources/templates/users/list.html b/src/main/resources/templates/users/list.html
index a0ef221..5cd63e2 100644
--- a/src/main/resources/templates/users/list.html
+++ b/src/main/resources/templates/users/list.html
@@ -5,7 +5,7 @@
사용자 목록
-
대상 공지 발송에 사용할 사용자 ID와 닉네임을 조회합니다.
+
ID 또는 닉네임으로 사용자를 조회합니다.
@@ -18,9 +18,9 @@
사용자 목록