Skip to content
73 changes: 64 additions & 9 deletions src/main/kotlin/codel/admin/presentation/AdminController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ class AdminController(
"PENDING" to adminService.countMembersByStatus("PENDING"),
"DONE" to adminService.countMembersByStatus("DONE"),
"REJECT" to adminService.countMembersByStatus("REJECT"),
"PHONE_VERIFIED" to adminService.countMembersByStatus("PHONE_VERIFIED")
"PHONE_VERIFIED" to adminService.countMembersByStatus("PHONE_VERIFIED"),
"WITHDRAWN" to adminService.countMembersByStatus("WITHDRAWN"),
"PERSONALITY_COMPLETED" to adminService.countMembersByStatus("PERSONALITY_COMPLETED")
)

model.addAttribute("members", members)
Expand Down Expand Up @@ -365,11 +367,9 @@ class AdminController(
val questions = adminService.findQuestionsWithFilter(keyword, category, isActive, pageable)
model.addAttribute("questions", questions)
model.addAttribute("categories", QuestionCategory.values())
model.addAttribute("param", mapOf(
"keyword" to (keyword ?: ""),
"category" to (category ?: ""),
"isActive" to (isActive?.toString() ?: "")
))
model.addAttribute("selectedKeyword", keyword ?: "")
model.addAttribute("selectedCategory", category ?: "")
model.addAttribute("selectedIsActive", isActive?.toString() ?: "")
return "questionList"
}

Expand Down Expand Up @@ -400,11 +400,21 @@ class AdminController(
@GetMapping("/v1/admin/questions/{questionId}/edit")
fun editQuestionForm(
@PathVariable questionId: Long,
@RequestParam(required = false) keyword: String?,
@RequestParam(required = false) category: String?,
@RequestParam(required = false) isActive: String?,
@RequestParam(required = false, defaultValue = "0") page: Int,
@RequestParam(required = false, defaultValue = "20") size: Int,
model: Model
): String {
val question = adminService.findQuestionById(questionId)
model.addAttribute("question", question)
model.addAttribute("categories", QuestionCategory.values())
model.addAttribute("filterKeyword", keyword)
model.addAttribute("filterCategory", category)
model.addAttribute("filterIsActive", isActive)
model.addAttribute("filterPage", page)
model.addAttribute("filterSize", size)
return "questionEditForm"
}

Expand All @@ -415,6 +425,11 @@ class AdminController(
@RequestParam category: String,
@RequestParam(required = false) description: String?,
@RequestParam(defaultValue = "false") isActive: Boolean,
@RequestParam(required = false) keyword: String?,
@RequestParam(required = false) filterCategory: String?,
@RequestParam(required = false) filterIsActive: String?,
@RequestParam(required = false, defaultValue = "0") page: Int,
@RequestParam(required = false, defaultValue = "20") size: Int,
redirectAttributes: RedirectAttributes
): String {
try {
Expand All @@ -424,12 +439,27 @@ class AdminController(
} catch (e: Exception) {
redirectAttributes.addFlashAttribute("error", "질문 수정에 실패했습니다: ${e.message}")
}
return "redirect:/v1/admin/questions"

// 필터 조건 유지하여 리다이렉트
val params = mutableListOf<String>()
keyword?.let { if (it.isNotBlank()) params.add("keyword=$it") }
filterCategory?.let { if (it.isNotBlank()) params.add("category=$it") }
filterIsActive?.let { if (it.isNotBlank()) params.add("isActive=$it") }
params.add("page=$page")
params.add("size=$size")

val queryString = if (params.isNotEmpty()) "?${params.joinToString("&")}" else ""
return "redirect:/v1/admin/questions$queryString"
}

@PostMapping("/v1/admin/questions/{questionId}/delete")
fun deleteQuestion(
@PathVariable questionId: Long,
@RequestParam(required = false) keyword: String?,
@RequestParam(required = false) category: String?,
@RequestParam(required = false) isActive: String?,
@RequestParam(required = false, defaultValue = "0") page: Int,
@RequestParam(required = false, defaultValue = "20") size: Int,
redirectAttributes: RedirectAttributes
): String {
try {
Expand All @@ -438,12 +468,27 @@ class AdminController(
} catch (e: Exception) {
redirectAttributes.addFlashAttribute("error", "질문 삭제에 실패했습니다: ${e.message}")
}
return "redirect:/v1/admin/questions"

// 필터 조건 유지하여 리다이렉트
val params = mutableListOf<String>()
keyword?.let { if (it.isNotBlank()) params.add("keyword=$it") }
category?.let { if (it.isNotBlank()) params.add("category=$it") }
isActive?.let { if (it.isNotBlank()) params.add("isActive=$it") }
params.add("page=$page")
params.add("size=$size")

val queryString = if (params.isNotEmpty()) "?${params.joinToString("&")}" else ""
return "redirect:/v1/admin/questions$queryString"
}

@PostMapping("/v1/admin/questions/{questionId}/toggle")
fun toggleQuestionStatus(
@PathVariable questionId: Long,
@RequestParam(required = false) keyword: String?,
@RequestParam(required = false) category: String?,
@RequestParam(required = false) isActive: String?,
@RequestParam(required = false, defaultValue = "0") page: Int,
@RequestParam(required = false, defaultValue = "20") size: Int,
redirectAttributes: RedirectAttributes
): String {
try {
Expand All @@ -453,7 +498,17 @@ class AdminController(
} catch (e: Exception) {
redirectAttributes.addFlashAttribute("error", "질문 상태 변경에 실패했습니다: ${e.message}")
}
return "redirect:/v1/admin/questions"

// 필터 조건 유지하여 리다이렉트
val params = mutableListOf<String>()
keyword?.let { if (it.isNotBlank()) params.add("keyword=$it") }
category?.let { if (it.isNotBlank()) params.add("category=$it") }
isActive?.let { if (it.isNotBlank()) params.add("isActive=$it") }
params.add("page=$page")
params.add("size=$size")

val queryString = if (params.isNotEmpty()) "?${params.joinToString("&")}" else ""
return "redirect:/v1/admin/questions$queryString"
}

// ========== 신고 관리 ==========
Expand Down
25 changes: 23 additions & 2 deletions src/main/kotlin/codel/member/business/MemberService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -701,11 +701,32 @@ class MemberService(
null
}

// 날짜 파싱 - yyyy-MM-dd 형식의 문자열을 LocalDateTime으로 변환
val startDateTime = if (!startDate.isNullOrBlank()) {
try {
LocalDate.parse(startDate).atStartOfDay() // 00:00:00
} catch (e: Exception) {
null
}
} else {
null
}

val endDateTime = if (!endDate.isNullOrBlank()) {
try {
LocalDate.parse(endDate).plusDays(1).atStartOfDay() // 다음 날 00:00:00 (해당일 23:59:59까지 포함)
} catch (e: Exception) {
null
}
} else {
null
}

// 정렬 처리를 위한 새로운 Pageable 생성
val sortedPageable = createSortedPageable(pageable, sort, direction)

// 새로운 메서드 사용
return memberJpaRepository.findMembersWithFilterAdvanced(keyword, statusEnum, sortedPageable)
// 새로운 메서드 사용 (날짜 파라미터 추가)
return memberJpaRepository.findMembersWithFilterAdvanced(keyword, statusEnum, startDateTime, endDateTime, sortedPageable)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,17 @@ interface MemberJpaRepository : JpaRepository<Member, Long> {
WHERE (:status IS NULL OR m.memberStatus = :status)
AND (
:keyword IS NULL OR :keyword = ''
OR LOWER(m.email) LIKE LOWER(CONCAT('%', :keyword, '%'))
OR LOWER(p.codeName) LIKE LOWER(CONCAT('%', :keyword, '%'))
)
AND (:startDate IS NULL OR m.createdAt >= :startDate)
AND (:endDate IS NULL OR m.createdAt < :endDate)
"""
)
fun findMembersWithFilterAdvanced(
@Param("keyword") keyword: String?,
@Param("status") status: MemberStatus?,
@Param("startDate") startDate: LocalDateTime?,
@Param("endDate") endDate: LocalDateTime?,
pageable: Pageable
): Page<Member>

Expand Down
28 changes: 26 additions & 2 deletions src/main/resources/templates/memberList.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,8 @@ <h4 th:text="${statusCounts.REJECT}">0</h4>
<small>거부됨</small>
</div>
<div class="col-md-2">
<h4 th:text="${statusCounts.PHONE_VERIFIED}">0</h4>
<small>핸드폰 인증 완료</small>
<h4 th:text="${statusCounts.WITHDRAWN}">0</h4>
<small>탈퇴</small>
</div>
<div class="col-md-2">
<h4 th:text="${#numbers.formatDecimal((statusCounts.DONE * 100.0) / (statusCounts.DONE + statusCounts.REJECT), 1, 1) + '%'}">0%</h4>
Expand All @@ -213,6 +213,8 @@ <h4 th:text="${#numbers.formatDecimal((statusCounts.DONE * 100.0) / (statusCount
<option th:selected="${param.status == 'DONE'}" value="DONE">승인 완료</option>
<option th:selected="${param.status == 'REJECT'}" value="REJECT">거부됨</option>
<option th:selected="${param.status == 'PHONE_VERIFIED'}" value="PHONE_VERIFIED">핸드폰 인증 완료</option>
<option th:selected="${param.status == 'PERSONALITY_COMPLETED'}" value="PERSONALITY_COMPLETED">오픈프로필 작성 완료</option>
<option th:selected="${param.status == 'WITHDRAWN'}" value="WITHDRAWN">탈퇴</option>
</select>
</div>
<div class="col-md-2">
Expand Down Expand Up @@ -295,6 +297,22 @@ <h4 th:text="${#numbers.formatDecimal((statusCounts.DONE * 100.0) / (statusCount
<span class="badge badge-signup ms-2" th:text="${statusCounts.PHONE_VERIFIED}">0</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center"
th:classappend="${param.status == 'PERSONALITY_COMPLETED'} ? 'active'"
th:href="@{/v1/admin/members(status='PERSONALITY_COMPLETED')}">
<span>오픈프로필 작성 완료</span>
<span class="badge bg-info ms-2" th:text="${statusCounts.PERSONALITY_COMPLETED}">0</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center"
th:classappend="${param.status == 'WITHDRAWN'} ? 'active'"
th:href="@{/v1/admin/members(status='WITHDRAWN')}">
<span>탈퇴</span>
<span class="badge bg-secondary ms-2" th:text="${statusCounts.WITHDRAWN}">0</span>
</a>
</li>
</ul>

<!-- 빠른 액션 바 -->
Expand Down Expand Up @@ -401,6 +419,12 @@ <h4 th:text="${#numbers.formatDecimal((statusCounts.DONE * 100.0) / (statusCount
<span class="badge badge-signup" th:case="'PHONE_VERIFIED'">
<i class="fa-solid fa-user-plus me-1"></i>핸드폰 인증 완료
</span>
<span class="badge bg-info text-white" th:case="'PERSONALITY_COMPLETED'">
<i class="fa-solid fa-user-edit me-1"></i>오픈프로필 작성 완료
</span>
<span class="badge bg-secondary" th:case="'WITHDRAWN'">
<i class="fa-solid fa-user-slash me-1"></i>탈퇴
</span>
<span class="badge bg-light text-dark" th:case="*" th:text="${member.memberStatus}"></span>
</span>
</td>
Expand Down
15 changes: 12 additions & 3 deletions src/main/resources/templates/questionEditForm.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,25 @@
<main class="col-md-10 ms-sm-auto px-md-5 py-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="fw-bold">질문 수정</h1>
<a href="/v1/admin/questions" class="btn btn-outline-secondary">
<a th:href="@{/v1/admin/questions(keyword=${filterKeyword}, category=${filterCategory}, isActive=${filterIsActive}, page=${filterPage}, size=${filterSize})}"
class="btn btn-outline-secondary">
<i class="fa-solid fa-arrow-left"></i> 목록으로
</a>
</div>

<div class="card form-card">
<div class="card-body">
<form th:action="@{/v1/admin/questions/{id}(id=${question.id})}" method="post">
<!-- 필터 파라미터 유지 -->
<input type="hidden" name="keyword" th:value="${filterKeyword}">
<input type="hidden" name="filterCategory" th:value="${filterCategory}">
<input type="hidden" name="filterIsActive" th:value="${filterIsActive}">
<input type="hidden" name="page" th:value="${filterPage}">
<input type="hidden" name="size" th:value="${filterSize}">

<div class="mb-4">
<label for="content" class="form-label fw-bold">질문 내용 <span class="text-danger">*</span></label>
<textarea class="form-control" id="content" name="content" rows="3"
<textarea class="form-control" id="content" name="content" rows="3"
placeholder="질문 내용을 입력하세요" required maxlength="500"
th:text="${question.content}"></textarea>
<div class="form-text">최대 500자까지 입력 가능합니다.</div>
Expand Down Expand Up @@ -91,7 +99,8 @@ <h1 class="fw-bold">질문 수정</h1>
<button type="submit" class="btn btn-primary">
<i class="fa-solid fa-save"></i> 수정
</button>
<a href="/v1/admin/questions" class="btn btn-outline-secondary">
<a th:href="@{/v1/admin/questions(keyword=${filterKeyword}, category=${filterCategory}, isActive=${filterIsActive}, page=${filterPage}, size=${filterSize})}"
class="btn btn-outline-secondary">
<i class="fa-solid fa-times"></i> 취소
</a>
</div>
Expand Down
Loading