Skip to content

Commit 3e086a9

Browse files
authored
Merge pull request #114 from wafflestudio/feat/admin
feat: add admin role, event ordering, period events
2 parents eb19ab1 + 94ff464 commit 3e086a9

11 files changed

Lines changed: 90 additions & 23 deletions

File tree

hangsha/batch/src/main/kotlin/com/team1/hangsha/batch/job/ExtraSnuSyncRunner.kt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import com.team1.hangsha.batch.crawler.ProgramEvent
77
import com.team1.hangsha.common.upload.OciUploadService
88
import com.team1.hangsha.event.dto.core.CrawledDetailSession
99
import com.team1.hangsha.event.dto.core.CrawledProgramEvent
10+
import com.team1.hangsha.event.model.EventPeriodPolicy
1011
import com.team1.hangsha.event.service.EventSyncService
1112
import org.springframework.boot.ApplicationArguments
1213
import org.springframework.boot.ApplicationRunner
1314
import org.springframework.stereotype.Component
1415
import java.nio.file.Files
1516
import java.nio.file.Path
17+
import java.time.LocalDate
1618
import kotlin.system.exitProcess
1719

1820
@Component
@@ -50,7 +52,9 @@ class ExtraSnuSyncRunner(
5052
val events = if (!opt.withDetails) {
5153
baseEvents
5254
} else {
53-
crawler.enrichDetails(baseEvents, ociUploadService) // { e -> e.status != "모집마감" } // @TODO: 위의 0001, 0002, ... 와 같이 매직 넘버라, ENUM화?
55+
crawler.enrichDetails(baseEvents, ociUploadService) { e ->
56+
!e.isPeriodEventFromList()
57+
}
5458
}
5559

5660
// dumpOnly 여부와 상관없이 이미지 업로드는 항상 수행한다.
@@ -132,6 +136,18 @@ private data class BatchArgs(
132136
}
133137
}
134138

139+
private fun ProgramEvent.isPeriodEventFromList(): Boolean {
140+
val title = title?.trim().orEmpty()
141+
val eventStart = activityStart?.let { LocalDate.parse(it).atStartOfDay() }
142+
val eventEnd = activityEnd?.let { LocalDate.parse(it).atTime(23, 59, 59) }
143+
144+
return EventPeriodPolicy.isPeriodEvent(
145+
title = title,
146+
eventStart = eventStart,
147+
eventEnd = eventEnd,
148+
)
149+
}
150+
135151
private fun ProgramEvent.toCrawledProgramEvent(): CrawledProgramEvent =
136152
CrawledProgramEvent(
137153
dataSeq = dataSeq,

hangsha/src/main/kotlin/com/team1/hangsha/config/SecurityConfig.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,11 @@ class SecurityConfig(
6363
// 주최 기관
6464
"/api/v1/category-groups/**",
6565
"/api/v1/categories/**",
66-
// admin
67-
"/admin/**",
6866
// 파일 업로드
6967
"/static/**",
7068
"/api/v1/uploads/oci/**",
7169
).permitAll()
70+
.requestMatchers("/admin/**").hasRole("ADMIN")
7271
.anyRequest().authenticated()
7372
}
7473
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)

hangsha/src/main/kotlin/com/team1/hangsha/event/repository/EventQueryRepository.kt

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class EventQueryRepository(
4040

4141
appendExcludedKeywordsFilter(userId)
4242

43-
append("\nORDER BY COALESCE(e.event_start, e.apply_start) ASC, e.id ASC")
43+
appendEventOrderBy(userId)
4444
}
4545

4646
val params = mutableMapOf<String, Any>(
@@ -130,7 +130,7 @@ class EventQueryRepository(
130130

131131
appendExcludedKeywordsFilter(userId)
132132

133-
append("\nORDER BY COALESCE(e.event_start, e.apply_start) DESC, e.id DESC")
133+
appendEventOrderBy(userId)
134134
append("\nLIMIT :limit OFFSET :offset")
135135
}
136136

@@ -252,4 +252,31 @@ private fun StringBuilder.appendExcludedKeywordsFilter(userId: Long?) {
252252
)
253253
""".trimIndent()
254254
)
255+
}
256+
257+
private fun StringBuilder.appendEventOrderBy(userId: Long?) {
258+
if (userId == null) {
259+
append("\nORDER BY COALESCE(e.event_start, e.apply_start) ASC, e.id ASC")
260+
return
261+
}
262+
263+
val matchedPriorityExpr = """
264+
(
265+
SELECT MIN(uic.priority)
266+
FROM user_interest_categories uic
267+
WHERE uic.user_id = :userId
268+
AND uic.category_id IN (e.status_id, e.event_type_id, e.org_id)
269+
)
270+
""".trimIndent()
271+
272+
append(
273+
"""
274+
275+
ORDER BY
276+
CASE WHEN $matchedPriorityExpr IS NULL THEN 1 ELSE 0 END ASC,
277+
$matchedPriorityExpr ASC,
278+
COALESCE(e.event_start, e.apply_start) ASC,
279+
e.id ASC
280+
""".trimIndent()
281+
)
255282
}

hangsha/src/main/kotlin/com/team1/hangsha/event/service/EventService.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,8 @@ class EventService(
6060
}
6161
}
6262

63-
fun effectiveStart(e: Event): LocalDateTime =
64-
listOfNotNull(e.applyStart, e.eventStart).minOrNull() ?: fromStart
65-
66-
fun effectiveEnd(e: Event): LocalDateTime =
67-
listOfNotNull(e.applyEnd, e.eventEnd).maxOrNull() ?: effectiveStart(e)
63+
fun sortStart(e: Event): LocalDateTime =
64+
e.eventStart ?: e.applyStart ?: fromStart
6865

6966
fun addRangeToBuckets(event: Event, start: LocalDateTime?, end: LocalDateTime?) {
7067
val rangeStart = start ?: return
@@ -100,7 +97,9 @@ class EventService(
10097
.toSortedMap()
10198
.mapValues { (_, dayEvents) ->
10299
val sorted = dayEvents.sortedWith(
103-
compareBy<Event> { effectiveStart(it) }.thenBy { it.id ?: Long.MAX_VALUE }
100+
compareBy<Event> { it.matchedInterestPriority(interestPriorityByCategoryId) ?: Int.MAX_VALUE }
101+
.thenBy { sortStart(it) }
102+
.thenBy { it.id ?: Long.MAX_VALUE }
104103
)
105104
MonthEventResponse.DayBucket(
106105
events = sorted.map { e ->

hangsha/src/main/kotlin/com/team1/hangsha/user/JwtAuthenticationFilter.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import jakarta.servlet.FilterChain
44
import jakarta.servlet.http.HttpServletRequest
55
import jakarta.servlet.http.HttpServletResponse
66
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
7+
import org.springframework.security.core.authority.SimpleGrantedAuthority
78
import org.springframework.security.core.context.SecurityContextHolder
89
import org.springframework.stereotype.Component
910
import org.springframework.util.AntPathMatcher
@@ -25,8 +26,10 @@ class JwtAuthenticationFilter(
2526

2627
if (token != null && jwtTokenProvider.validateAccessToken(token)) {
2728
val userId = jwtTokenProvider.getUserId(token)
29+
val isAdmin = jwtTokenProvider.getIsAdmin(token)
30+
val authorities = if (isAdmin) listOf(SimpleGrantedAuthority("ROLE_ADMIN")) else emptyList()
2831

29-
val authentication = UsernamePasswordAuthenticationToken(userId, null, emptyList())
32+
val authentication = UsernamePasswordAuthenticationToken(userId, null, authorities)
3033
SecurityContextHolder.getContext().authentication = authentication
3134
request.setAttribute("userId", userId)
3235
}
@@ -38,4 +41,4 @@ class JwtAuthenticationFilter(
3841
val bearerToken = request.getHeader("Authorization") ?: return null
3942
return if (bearerToken.startsWith("Bearer ")) bearerToken.substring(7) else null
4043
}
41-
}
44+
}

hangsha/src/main/kotlin/com/team1/hangsha/user/JwtTokenProvider.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ class JwtTokenProvider(
2323

2424
private val key = Keys.hmacShaKeyFor(secretKey.toByteArray())
2525

26-
fun createAccessToken(userId: Long): String {
26+
fun createAccessToken(userId: Long, isAdmin: Boolean = false): String {
2727
return createToken(
2828
userId = userId,
29+
isAdmin = isAdmin,
2930
expirationMs = accessExpirationMs,
3031
type = "ACCESS",
3132
)
@@ -36,11 +37,13 @@ class JwtTokenProvider(
3637
userId = userId,
3738
expirationMs = refreshExpirationMs,
3839
type = "REFRESH",
40+
isAdmin = false,
3941
)
4042
}
4143

4244
fun createToken(
4345
userId: Long,
46+
isAdmin: Boolean,
4447
expirationMs: Long,
4548
type: String
4649
): String {
@@ -54,6 +57,7 @@ class JwtTokenProvider(
5457
.setId(jti)
5558
.claim("jti", jti)
5659
.claim("type", type)
60+
.claim("isAdmin", isAdmin)
5761
.setIssuedAt(now)
5862
.setExpiration(expiry)
5963
.signWith(key, SignatureAlgorithm.HS256)
@@ -70,6 +74,9 @@ class JwtTokenProvider(
7074
fun getUserId(token: String): Long =
7175
parseClaims(token).subject.toLong()
7276

77+
fun getIsAdmin(token: String): Boolean =
78+
parseClaims(token)["isAdmin"] as? Boolean ?: false
79+
7380
fun validateAccessToken(token: String): Boolean {
7481
try {
7582
val claims = Jwts
@@ -113,4 +120,4 @@ class JwtTokenProvider(
113120

114121
fun getJti(token: String): String =
115122
parseClaims(token).id
116-
}
123+
}

hangsha/src/main/kotlin/com/team1/hangsha/user/handler/OAuth2SuccessHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class OAuth2SuccessHandler(
3939
val user = userRepository.findByEmail(email)
4040
?: throw RuntimeException("User not found after OAuth2 login")
4141

42-
val accessToken = jwtTokenProvider.createAccessToken(user.id!!)
42+
val accessToken = jwtTokenProvider.createAccessToken(user.id!!, user.isAdmin)
4343
val refreshToken = jwtTokenProvider.createRefreshToken(user.id!!)
4444
saveRefresh(user.id!!, refreshToken)
4545

@@ -71,4 +71,4 @@ class OAuth2SuccessHandler(
7171
)
7272
)
7373
}
74-
}
74+
}

hangsha/src/main/kotlin/com/team1/hangsha/user/model/User.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ data class User (
1414
var email: String? = null,
1515
@Column("profile_image_url")
1616
var profileImageUrl: String? = null,
17+
@Column("is_admin")
18+
var isAdmin: Boolean = false,
1719
@CreatedDate
1820
var createdAt: Instant? = null,
1921
@LastModifiedDate
2022
var updatedAt: Instant? = null,
21-
)
23+
)

hangsha/src/main/kotlin/com/team1/hangsha/user/service/UserService.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,10 @@ class UserService(
9999
if (!BCrypt.checkpw(password, hashed)) throw DomainException(ErrorCode.AUTH_INVALID_CREDENTIALS)
100100

101101
val userId = identity.userId
102+
val user = userRepository.findById(userId)
103+
.orElseThrow { DomainException(ErrorCode.USER_NOT_FOUND) }
102104

103-
val access = jwtTokenProvider.createAccessToken(userId)
105+
val access = jwtTokenProvider.createAccessToken(userId, user.isAdmin)
104106
val refresh = jwtTokenProvider.createRefreshToken(userId)
105107

106108
saveRefresh(userId, refresh)
@@ -115,7 +117,10 @@ class UserService(
115117

116118
@Transactional
117119
fun issueAfterSocialLogin(userId: Long): IssuedTokens {
118-
val access = jwtTokenProvider.createAccessToken(userId)
120+
val user = userRepository.findById(userId)
121+
.orElseThrow { DomainException(ErrorCode.USER_NOT_FOUND) }
122+
123+
val access = jwtTokenProvider.createAccessToken(userId, user.isAdmin)
119124
val refresh = jwtTokenProvider.createRefreshToken(userId)
120125

121126
saveRefresh(userId, refresh)
@@ -151,7 +156,10 @@ class UserService(
151156
throw DomainException(ErrorCode.AUTH_INVALID_TOKEN)
152157
}
153158

154-
val newAccess = jwtTokenProvider.createAccessToken(userId)
159+
val user = userRepository.findById(userId)
160+
.orElseThrow { DomainException(ErrorCode.USER_NOT_FOUND) }
161+
162+
val newAccess = jwtTokenProvider.createAccessToken(userId, user.isAdmin)
155163
val newRefresh = jwtTokenProvider.createRefreshToken(userId)
156164

157165
saveRefresh(userId, newRefresh)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ALTER TABLE users
2+
ADD COLUMN is_admin TINYINT(1) NOT NULL DEFAULT 0 AFTER profile_image_url;
3+
4+
UPDATE users
5+
SET is_admin = 1
6+
WHERE email = 'admin@hangsha.local';

0 commit comments

Comments
 (0)