Skip to content
Merged

Main #261

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
df817eb
Merge pull request #212 from GDGoCINHA/develop
pwc2002 Aug 30, 2025
56118df
Merge branch 'develop'
kaswhy Sep 3, 2025
de3958f
Merge pull request #218 from GDGoCINHA/develop
pwc2002 Sep 7, 2025
6c7ddd4
Merge pull request #219 from GDGoCINHA/develop
pwc2002 Sep 7, 2025
2500e13
Merge pull request #222 from GDGoCINHA/develop
CSE-Shaco Oct 21, 2025
7434cb6
Merge pull request #224 from GDGoCINHA/develop
CSE-Shaco Oct 21, 2025
78ee5dd
Merge pull request #226 from GDGoCINHA/develop
CSE-Shaco Oct 21, 2025
c2b44d7
Merge pull request #228 from GDGoCINHA/develop
CSE-Shaco Oct 21, 2025
bedf24b
Merge pull request #230 from GDGoCINHA/develop
CSE-Shaco Oct 21, 2025
f4e4c54
Feat: ๊ถŒํ•œ ๊ฒ€์ฆ ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€ ๋ฐ UserRole ๊ธฐ๋ฐ˜ ๋น„๊ต ๋กœ์ง ๊ฐœ์„ 
CSE-Shaco Oct 22, 2025
42115a2
Merge pull request #231 from CSE-Shaco/develop
CSE-Shaco Oct 22, 2025
ce65cfb
fix(core-attendance): Postgres unnest ํŒŒ๋ผ๋ฏธํ„ฐ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ โ€” ๋ฐฐ์—ด+์บ์ŠคํŒ… ์ ์šฉ
CSE-Shaco Oct 23, 2025
36aabf1
Merge pull request #232 from CSE-Shaco/develop
CSE-Shaco Oct 23, 2025
4c41932
feat(core-attendance): refine controller endpoints & auth-aware team โ€ฆ
CSE-Shaco Oct 23, 2025
7850eba
Merge pull request #233 from CSE-Shaco/develop
CSE-Shaco Oct 23, 2025
047771f
feat(core-attendance): ๋‚ ์งœ ์ƒ์„ฑ ๊ถŒํ•œ ์ฒดํฌ ์ถ”๊ฐ€
CSE-Shaco Oct 24, 2025
b9c593c
Merge pull request #234 from CSE-Shaco/develop
CSE-Shaco Oct 24, 2025
c1af13a
feat(user-admin): ์‚ฌ์šฉ์ž ๋ชฉ๋ก/์—ญํ• ยทํŒ€ ์ˆ˜์ • API ์ถ”๊ฐ€
CSE-Shaco Oct 26, 2025
67e9860
refactor(security): JWT/Principal์— team ๋…ธ์ถœ ๋ฐ ๊ถŒํ•œ ์ฒดํฌ ๊ฐœ์„ 
CSE-Shaco Oct 26, 2025
18f9d0d
feat(recruit): User Role ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ๊ถŒํ•œ ์ˆ˜์ •
CSE-Shaco Oct 26, 2025
37f1a97
refactor(user): ์—ญํ• /ํŒ€ ๋ณ€๊ฒฝ ๋„๋ฉ”์ธ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
CSE-Shaco Oct 26, 2025
eb67c74
feat(user-repo): ์‚ฌ์šฉ์ž ์š”์•ฝ ํŽ˜์ด์ง€ ์กฐํšŒ/๊ฒ€์ƒ‰/์ •๋ ฌ ์ง€์›
CSE-Shaco Oct 26, 2025
e850fd1
Merge pull request #235 from CSE-Shaco/develop
CSE-Shaco Oct 26, 2025
d77532f
feat(user-admin): ์‚ฌ์šฉ์ž ์‚ญ์ œ ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€ (Controller ๋ฐ Service ๊ณ„์ธต)
CSE-Shaco Oct 26, 2025
46ffbe9
Merge pull request #236 from CSE-Shaco/develop
CSE-Shaco Oct 26, 2025
8173d24
fix(user-admin): User role ์ •๋ ฌ ์ ์šฉ pagination
CSE-Shaco Oct 27, 2025
ecde934
Merge pull request #237 from CSE-Shaco/develop
CSE-Shaco Oct 27, 2025
3af9d71
fix(user-admin): ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์ •๋ ฌ ๋ฐ ๊ถŒํ•œ๋ณ„ ์ ‘๊ทผ ๋กœ์ง ๊ฐœ์„ 
CSE-Shaco Oct 27, 2025
1b56c3f
Merge pull request #238 from CSE-Shaco/develop
CSE-Shaco Oct 27, 2025
48e7866
fix(user-admin): ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์ •๋ ฌ ๋ฐ ๊ถŒํ•œ๋ณ„ ์ ‘๊ทผ ๋กœ์ง ๊ฐœ์„ 
CSE-Shaco Oct 27, 2025
5be71d5
Merge pull request #239 from CSE-Shaco/develop
CSE-Shaco Oct 27, 2025
cebec41
fix(core-attendance): HR ๋ฆฌ๋“œ ๊ถŒํ•œ ํ™•๋Œ€
CSE-Shaco Oct 27, 2025
6d8283b
Merge pull request #240 from CSE-Shaco/develop
CSE-Shaco Oct 27, 2025
29f5232
fix(user-admin): ๊ถŒํ•œ๋ณ„ ์ •๋ ฌ ๊ฐœ์„ 
CSE-Shaco Oct 27, 2025
979c325
Merge pull request #241 from CSE-Shaco/develop
CSE-Shaco Oct 27, 2025
4d60508
fix: ์˜ค๊ฑฐ๋‚˜์ด์ € ์ „์šฉ ํŒ€ HQ ์ถ”๊ฐ€
CSE-Shaco Nov 5, 2025
2999eda
Merge pull request #242 from CSE-Shaco/develop
CSE-Shaco Nov 5, 2025
3fe4a24
fix(core-attendance): ์ถœ์„์ฒดํฌ ๋ฆฌ์ŠคํŠธ์— ์˜ค๊ฑฐ๋‚˜์ด์ € ์ถ”๊ฐ€
CSE-Shaco Nov 5, 2025
75e98dc
Merge pull request #243 from CSE-Shaco/develop
CSE-Shaco Nov 5, 2025
8b5d955
fix(core-attendance): ์ถœ์„์ฒดํฌ ๋ฆฌ์ŠคํŠธ์— ์˜ค๊ฑฐ๋‚˜์ด์ € ์ถ”๊ฐ€
CSE-Shaco Nov 5, 2025
3b6e29b
Merge pull request #244 from CSE-Shaco/develop
CSE-Shaco Nov 5, 2025
055a30e
fix(core-attendance): csv ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ ๊ฐœ์„ 
CSE-Shaco Nov 5, 2025
52911db
Merge pull request #245 from CSE-Shaco/develop
CSE-Shaco Nov 5, 2025
d10f5b0
fix(core-attendance): fix summary content
CSE-Shaco Nov 27, 2025
26c390f
fix(global-exception): fix FORBIDDEN error code
CSE-Shaco Nov 27, 2025
9a39e24
feat(manito): ๋งˆ๋‹ˆ๋˜ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
CSE-Shaco Nov 27, 2025
4ad0339
Merge pull request #246 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
490a4b2
fix(manito): request ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ
CSE-Shaco Nov 27, 2025
11755ef
Merge pull request #247 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
dfd3ea3
fix(manito): csv ํŒŒ์ผ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์ˆ˜์ •
CSE-Shaco Nov 27, 2025
7fd3f8e
Merge pull request #248 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
5016413
fix(manito): csv ํŒŒ์ผ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์ˆ˜์ •
CSE-Shaco Nov 27, 2025
65024d9
Merge pull request #249 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
fbc0da1
fix(manito): csv ํŒŒ์ผ ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์ˆ˜์ •
CSE-Shaco Nov 27, 2025
ad9f495
Merge pull request #250 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
c834a3e
fix(manito): ๋ฏธ์ƒ ์˜ค๋ฅ˜ ์ˆ˜์ •์ค‘
CSE-Shaco Nov 27, 2025
6aabd61
Merge pull request #251 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
e459f04
fix(manito): ๋ฏธ์ƒ ์˜ค๋ฅ˜ ์ˆ˜์ •์ค‘
CSE-Shaco Nov 27, 2025
0fe3c76
Merge pull request #252 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
74125ce
fix: ๋””๋ฒ„๊น… ์„ค์ • ๋ณ€๊ฒฝ
CSE-Shaco Nov 27, 2025
c768f4b
Merge pull request #253 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
2a967f9
fix: ๋””๋ฒ„๊น… ์„ค์ • ๋ณ€๊ฒฝ
CSE-Shaco Nov 27, 2025
c9cb45a
Merge pull request #254 from CSE-Shaco/develop
CSE-Shaco Nov 27, 2025
dd5abf8
fix(manito): ํ•€ ์ •๊ทœํ™” ๋กœ์ง ์˜์กด์„ฑ ์ถ”๊ฐ€
CSE-Shaco Nov 29, 2025
9e592c6
Merge pull request #255 from CSE-Shaco/develop
CSE-Shaco Nov 29, 2025
eeff9c8
fix(security-config): ๋งˆ๋‹ˆ๋˜ ํ™•์ธ ํŽ˜์ด์ง€ ํ•„ํ„ฐ๋ง
CSE-Shaco Nov 29, 2025
8581fb0
Merge pull request #256 from CSE-Shaco/develop
CSE-Shaco Nov 29, 2025
c78bc0a
fix(manito): ํ•€ ํ™•์ธ ๋กœ๊ทธ
CSE-Shaco Nov 29, 2025
7e7960a
Merge pull request #257 from CSE-Shaco/develop
CSE-Shaco Nov 29, 2025
5453dc1
fix(manito): Lob ์‚ญ์ œ ๋ฐ Transactional ์ถ”๊ฐ€
CSE-Shaco Nov 29, 2025
74d0a11
Merge pull request #258 from CSE-Shaco/develop
CSE-Shaco Nov 29, 2025
e2133cb
fix(manito): unexpected token ํ•ด๊ฒฐ
CSE-Shaco Nov 29, 2025
212bb88
Merge pull request #259 from CSE-Shaco/develop
CSE-Shaco Nov 29, 2025
5ebaf66
fix(manito): UI/UX ๊ฐœ์„ ์— ๋”ฐ๋ฅธ ๋ณ€๊ฒฝ์‚ฌํ•ญ ๋ฐ˜์˜
CSE-Shaco Nov 29, 2025
ea45ed7
Merge pull request #260 from CSE-Shaco/develop
CSE-Shaco Nov 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 62 additions & 49 deletions src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
package inha.gdgoc.domain.auth.controller;

import static inha.gdgoc.domain.auth.controller.message.AuthMessage.ACCESS_TOKEN_REFRESH_SUCCESS;
import static inha.gdgoc.domain.auth.controller.message.AuthMessage.CODE_CREATION_SUCCESS;
import static inha.gdgoc.domain.auth.controller.message.AuthMessage.LOGIN_WITH_PASSWORD_SUCCESS;
import static inha.gdgoc.domain.auth.controller.message.AuthMessage.LOGOUT_SUCCESS;
import static inha.gdgoc.domain.auth.controller.message.AuthMessage.OAUTH_LOGIN_SIGNUP_SUCCESS;
import static inha.gdgoc.domain.auth.controller.message.AuthMessage.PASSWORD_CHANGE_SUCCESS;
import static inha.gdgoc.domain.auth.controller.message.AuthMessage.PASSWORD_RESET_VERIFICATION_SUCCESS;
import static inha.gdgoc.domain.auth.exception.AuthErrorCode.UNAUTHORIZED_USER;
import static inha.gdgoc.domain.auth.exception.AuthErrorCode.USER_NOT_FOUND;

import inha.gdgoc.domain.auth.dto.request.CodeVerificationRequest;
import inha.gdgoc.domain.auth.dto.request.PasswordResetRequest;
import inha.gdgoc.domain.auth.dto.request.SendingCodeRequest;
Expand All @@ -24,29 +14,32 @@
import inha.gdgoc.domain.auth.service.MailService;
import inha.gdgoc.domain.auth.service.RefreshTokenService;
import inha.gdgoc.domain.user.entity.User;
import inha.gdgoc.domain.user.enums.TeamType;
import inha.gdgoc.domain.user.enums.UserRole;
import inha.gdgoc.domain.user.repository.UserRepository;
import inha.gdgoc.global.config.jwt.TokenProvider;
import inha.gdgoc.global.dto.response.ApiResponse;
import inha.gdgoc.global.exception.GlobalErrorCode;
import jakarta.servlet.http.HttpServletResponse;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Optional;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Optional;

import static inha.gdgoc.domain.auth.controller.message.AuthMessage.*;
import static inha.gdgoc.domain.auth.exception.AuthErrorCode.UNAUTHORIZED_USER;
import static inha.gdgoc.domain.auth.exception.AuthErrorCode.USER_NOT_FOUND;

@Slf4j
@RequestMapping("/api/v1/auth")
Expand All @@ -61,19 +54,14 @@ public class AuthController {
private final AuthCodeService authCodeService;

@GetMapping("/oauth2/google/callback")
public ResponseEntity<ApiResponse<Map<String, Object>, Void>> handleGoogleCallback(
@RequestParam String code,
HttpServletResponse response
) {
public ResponseEntity<ApiResponse<Map<String, Object>, Void>> handleGoogleCallback(@RequestParam String code, HttpServletResponse response) {
Map<String, Object> data = authService.processOAuthLogin(code, response);

return ResponseEntity.ok(ApiResponse.ok(OAUTH_LOGIN_SIGNUP_SUCCESS, data));
}

@PostMapping("/refresh")
public ResponseEntity<?> refreshAccessToken(
@CookieValue(value = "refresh_token", required = false) String refreshToken
) {
public ResponseEntity<?> refreshAccessToken(@CookieValue(value = "refresh_token", required = false) String refreshToken) {
log.info("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์š”์ฒญ ๋ฐ›์Œ. ํ† ํฐ ์กด์žฌ ์—ฌ๋ถ€: {}", refreshToken != null);

if (refreshToken == null) {
Expand All @@ -84,19 +72,15 @@ public ResponseEntity<?> refreshAccessToken(
String newAccessToken = refreshTokenService.refreshAccessToken(refreshToken);
AccessTokenResponse accessTokenResponse = new AccessTokenResponse(newAccessToken);

return ResponseEntity.ok(
ApiResponse.ok(ACCESS_TOKEN_REFRESH_SUCCESS, accessTokenResponse, null));
return ResponseEntity.ok(ApiResponse.ok(ACCESS_TOKEN_REFRESH_SUCCESS, accessTokenResponse, null));
} catch (Exception e) {
log.error("๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {}", e.getMessage(), e);
throw new AuthException(AuthErrorCode.INVALID_REFRESH_TOKEN);
}
}

@PostMapping("/login")
public ResponseEntity<ApiResponse<LoginResponse, Void>> login(
@Valid @RequestBody UserLoginRequest req,
HttpServletResponse response
) throws NoSuchAlgorithmException, InvalidKeyException {
public ResponseEntity<ApiResponse<LoginResponse, Void>> login(@Valid @RequestBody UserLoginRequest req, HttpServletResponse response) throws NoSuchAlgorithmException, InvalidKeyException {
String email = req.email().trim();
LoginResponse loginResponse = authService.loginWithPassword(email, req.password(), response);
return ResponseEntity.ok(ApiResponse.ok(LOGIN_WITH_PASSWORD_SUCCESS, loginResponse));
Expand All @@ -109,9 +93,7 @@ public ResponseEntity<ApiResponse<Void, Void>> logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

// 1) ์ต๋ช… ๋ฐฉ์–ด
if (authentication == null
|| !authentication.isAuthenticated()
|| "anonymousUser".equals(authentication.getName())) {
if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) {
throw new AuthException(UNAUTHORIZED_USER);
}

Expand Down Expand Up @@ -142,12 +124,9 @@ public ResponseEntity<ApiResponse<Void, Void>> logout() {
}

@PostMapping("/password-reset/request")
public ResponseEntity<ApiResponse<Void, Void>> responseResponseEntity(
@RequestBody SendingCodeRequest sendingCodeRequest
) {
public ResponseEntity<ApiResponse<Void, Void>> responseResponseEntity(@RequestBody SendingCodeRequest sendingCodeRequest) {
// TODO ์„œ๋น„์Šค๋กœ ๋„˜๊ธฐ๊ธฐ
if (userRepository.existsByNameAndEmail(sendingCodeRequest.name(),
sendingCodeRequest.email())) {
if (userRepository.existsByNameAndEmail(sendingCodeRequest.name(), sendingCodeRequest.email())) {
String code = mailService.sendAuthCode(sendingCodeRequest.email());
authCodeService.saveAuthCode(sendingCodeRequest.email(), code);

Expand All @@ -157,9 +136,7 @@ public ResponseEntity<ApiResponse<Void, Void>> responseResponseEntity(
}

@PostMapping("/password-reset/verify")
public ResponseEntity<ApiResponse<CodeVerificationResponse, Void>> verifyCode(
@RequestBody CodeVerificationRequest request
) {
public ResponseEntity<ApiResponse<CodeVerificationResponse, Void>> verifyCode(@RequestBody CodeVerificationRequest request) {
// TODO ์„œ๋น„์Šค ๋‹จ DTO ์ถ”๊ฐ€
boolean verified = authCodeService.verify(request.email(), request.code());
CodeVerificationResponse response = new CodeVerificationResponse(verified);
Expand All @@ -168,9 +145,7 @@ public ResponseEntity<ApiResponse<CodeVerificationResponse, Void>> verifyCode(
}

@PostMapping("/password-reset/confirm")
public ResponseEntity<ApiResponse<Void, Void>> resetPassword(
@RequestBody PasswordResetRequest passwordResetRequest
) throws NoSuchAlgorithmException, InvalidKeyException {
public ResponseEntity<ApiResponse<Void, Void>> resetPassword(@RequestBody PasswordResetRequest passwordResetRequest) throws NoSuchAlgorithmException, InvalidKeyException {
// TODO ์„œ๋น„์Šค ๋‹จ์œผ๋กœ
Optional<User> user = userRepository.findByEmail(passwordResetRequest.email());
if (user.isEmpty()) {
Expand All @@ -183,4 +158,42 @@ public ResponseEntity<ApiResponse<Void, Void>> resetPassword(

return ResponseEntity.ok(ApiResponse.ok(PASSWORD_CHANGE_SUCCESS));
}

/**
* ์š”๊ตฌ ๊ถŒํ•œ(role) ์ด์ƒ์ด๋ฉด 200, ์•„๋‹ˆ๋ฉด 403
* ๋ฏธ์ธ์ฆ์ด๋ฉด 401

* ์˜ˆ) /api/v1/auth/LEAD, /api/v1/auth/ORGANIZER, /api/v1/auth/ADMIN
*/
@GetMapping("/{role}")
public ResponseEntity<ApiResponse<Void, ?>> checkRoleOrTeam(@AuthenticationPrincipal TokenProvider.CustomUserDetails me, @PathVariable UserRole role, @RequestParam(value = "team", required = false) TeamType requiredTeam) {
// 1) ์ธ์ฆ ์ฒดํฌ
if (me == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(GlobalErrorCode.UNAUTHORIZED_USER.getStatus()
.value(), GlobalErrorCode.UNAUTHORIZED_USER.getMessage(), null));
}

// 2) role check
final boolean roleOk = UserRole.hasAtLeast(me.getRole(), role);

// 3) team check if team parameter exists
boolean teamOk = false;
if (requiredTeam != null) {
if (UserRole.hasAtLeast(me.getRole(), UserRole.ORGANIZER)) {
teamOk = true;
} else {
teamOk = (me.getTeam() != null && me.getTeam() == requiredTeam);
}
}

// 4) OR ์กฐ๊ฑด์œผ๋กœ ์ตœ์ข… ํŒ์ •
if (roleOk || teamOk) {
return ResponseEntity.ok(ApiResponse.ok("ROLE_OR_TEAM_CHECK_PASSED", null));
}

return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.error(GlobalErrorCode.FORBIDDEN_USER.getStatus()
.value(), GlobalErrorCode.FORBIDDEN_USER.getMessage(), null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,14 @@ public ResponseEntity<ApiResponse<DateListResponse, Void>> listDates() {
return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.DATE_LIST_RETRIEVED_SUCCESS, new DateListResponse(service.getDates())));
}

@PreAuthorize("hasAnyRole('ORGANIZER', 'ADMIN')")
@PostMapping
public ResponseEntity<ApiResponse<DateListResponse, Void>> createDate(@Valid @RequestBody CreateDateRequest request) {
service.addDate(request.getDate());
return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.DATE_CREATED_SUCCESS, new DateListResponse(service.getDates())));
}

@PreAuthorize("hasAnyRole('ORGANIZER', 'ADMIN')")
@DeleteMapping("/{date}")
public ResponseEntity<ApiResponse<DateListResponse, Void>> deleteDate(@PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
service.deleteDate(date.toString());
Expand All @@ -66,7 +68,7 @@ public ResponseEntity<ApiResponse<DateListResponse, Void>> deleteDate(@PathVaria
/* ===== ํŒ€ ๋ชฉ๋ก (๋ฆฌ๋“œ=๋ณธ์ธ ํŒ€๋งŒ / ๊ด€๋ฆฌ์ž=์ „์ฒด) ===== */
@GetMapping("/teams")
public ResponseEntity<ApiResponse<List<TeamResponse>, PageMeta>> getTeams(@AuthenticationPrincipal CustomUserDetails me) {
List<TeamResponse> list = (me.getRole() == UserRole.LEAD) ? service.getTeamsForLead(requiredTeamFrom(me)) : service.getTeamsForOrganizerOrAdmin();
List<TeamResponse> list = (me.getRole() == UserRole.LEAD && me.getTeam() != TeamType.HR) ? service.getTeamsForLead(requiredTeamFrom(me)) : service.getTeamsForOrganizerOrAdmin();

var page = new PageImpl<>(list, PageRequest.of(0, Math.max(1, list.size()), Sort.by(Sort.Direction.DESC, "createdAt")), list.size());
return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.TEAM_LIST_RETRIEVED_SUCCESS, list, PageMeta.of(page)));
Expand All @@ -77,7 +79,7 @@ public ResponseEntity<ApiResponse<List<TeamResponse>, PageMeta>> getTeams(@Authe
@GetMapping("/{date}/members")
public ResponseEntity<ApiResponse<List<Map<String, Object>>, Void>> membersOfMeeting(@AuthenticationPrincipal CustomUserDetails me, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam(required = false) TeamType team // ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉ, ๋ฆฌ๋“œ๋Š” ๋ฌด์‹œ
) {
TeamType effectiveTeam = (me.getRole() == UserRole.LEAD) ? requiredTeamFrom(me) : team;
TeamType effectiveTeam = (me.getRole() == UserRole.LEAD && me.getTeam() != TeamType.HR) ? requiredTeamFrom(me) : team;
var list = service.getMembersWithPresence(date.toString(), effectiveTeam);
// list ์›์†Œ ์˜ˆ์‹œ: { "userId": "123", "name": "ํ™๊ธธ๋™", "present": true, "lastModifiedAt": "..." }
return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.TEAM_LIST_RETRIEVED_SUCCESS, list));
Expand All @@ -86,11 +88,11 @@ public ResponseEntity<ApiResponse<List<Map<String, Object>>, Void>> membersOfMee
/* ===== ํŠน์ • ๋‚ ์งœ ์ถœ์„ ์ผ๊ด„ ์ €์žฅ (๋ฉฑ๋“ฑ ์Šค๋ƒ…์ƒท) ===== */
// Body: { "userIds": ["1","2",...], "present": true } โ†’ presentUserIds๋งŒ ๋ณด๋‚ด๋Š” ๊ตฌ์กฐ๋กœ๋„ ์‰ฝ๊ฒŒ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ
@PutMapping("/{date}/attendance")
public ResponseEntity<ApiResponse<Map<String, Object>, Void>> saveAttendanceSnapshot(@AuthenticationPrincipal CustomUserDetails me, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam(required = false) TeamType team, // ๊ด€๋ฆฌ์ž๋งŒ ์‚ฌ์šฉ, ๋ฆฌ๋“œ๋Š” ๋ฌด์‹œ
@RequestBody @Valid SetAttendanceRequest req) {
public ResponseEntity<ApiResponse<Map<String, Object>, Void>> saveAttendanceSnapshot(@AuthenticationPrincipal CustomUserDetails me, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestBody @Valid SetAttendanceRequest req) {
var userIds = req.safeUserIds();

if (me.getRole() == UserRole.LEAD) {
// LEAD โ†’ ๋ณธ์ธ ํŒ€ ๊ฒ€์ฆ
if (me.getRole() == UserRole.LEAD && me.getTeam() != TeamType.HR) {
TeamType myTeam = requiredTeamFrom(me);
var validation = service.filterUserIdsNotInTeam(myTeam, userIds);
if (validation.validIds().isEmpty()) {
Expand All @@ -100,32 +102,42 @@ public ResponseEntity<ApiResponse<Map<String, Object>, Void>> saveAttendanceSnap
return okUpdated(updated, validation.invalidIds());
}

// ORGANIZER / ADMIN
TeamType effectiveTeam = (team != null) ? team : service.inferTeamFromUserIds(userIds)
.orElseThrow(() -> new IllegalArgumentException("userIds๋กœ ํŒ€์„ ์ถ”๋ก ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."));

var validation = service.filterUserIdsNotInTeam(effectiveTeam, userIds);
if (validation.validIds().isEmpty()) {
return okUpdated(0L, validation.invalidIds());
}
long updated = service.setAttendance(date.toString(), validation.validIds(), req.presentValue());
return okUpdated(updated, validation.invalidIds());
// ORGANIZER / ADMIN โ†’ ํŒ€ ์ถ”๋ก /๊ฒ€์ฆ ์—†์ด ๋ฐ”๋กœ ์—…์„œํŠธ
long updated = service.setAttendance(date.toString(), userIds, req.presentValue());
return okUpdated(updated, List.of());
}

/* ===== ๋‚ ์งœ ์š”์•ฝ(JSON) ===== */
@GetMapping("/{date}/summary")
public ResponseEntity<ApiResponse<DaySummaryResponse, Void>> summary(@AuthenticationPrincipal CustomUserDetails me, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam(required = false) TeamType team) {
DaySummaryResponse body = (me.getRole() == UserRole.LEAD) ? service.summary(date.toString(), requiredTeamFrom(me)) : service.summary(date.toString(), team);
DaySummaryResponse body = (me.getRole() == UserRole.LEAD && me.getTeam() != TeamType.HR) ? service.summary(date.toString(), requiredTeamFrom(me)) : service.summary(date.toString(), team);
return ResponseEntity.ok(ApiResponse.ok(CoreAttendanceMessage.SUMMARY_RETRIEVED_SUCCESS, body));
}

/* ===== ๋‚ ์งœ ์š”์•ฝ(CSV) ===== */
@GetMapping(value = "/{date}/summary.csv", produces = "text/csv; charset=UTF-8")
public ResponseEntity<String> summaryCsv(@AuthenticationPrincipal CustomUserDetails me, @PathVariable @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, @RequestParam(required = false) TeamType team) {
TeamType effective = (me.getRole() == UserRole.LEAD) ? requiredTeamFrom(me) : team;
String csv = service.buildSummaryCsv(date.toString(), effective); // ์„œ๋น„์Šค์— ๊ตฌํ˜„
TeamType effective = (me.getRole() == UserRole.LEAD && me.getTeam() != TeamType.HR) ? requiredTeamFrom(me) : team;
String csv = service.buildSummaryCsv(date.toString(), effective);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"attendance-" + date + ".csv\"")
.body(csv);
}

@GetMapping(value = "/summary.csv", produces = "text/csv; charset=UTF-8")
public ResponseEntity<String> summaryCsvAll(
@AuthenticationPrincipal CustomUserDetails me,
@RequestParam(required = false) TeamType team
) {
// LEAD & not HR โ†’ ์ž์‹ ์˜ ํŒ€๋งŒ
TeamType effective = (me.getRole() == UserRole.LEAD && me.getTeam() != TeamType.HR)
? requiredTeamFrom(me)
: team;

String csv = service.buildFullMatrixCsv(effective);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"attendance-summary.csv\"")
.body(csv);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@ public interface AttendanceRecordRepository extends JpaRepository<AttendanceReco
/* ๋ฐฐ์น˜ ์—…์„œํŠธ(ON CONFLICT) โ€” meeting_id ๊ธฐ์ค€ */
@Modifying
@Query(value = """
INSERT INTO public.attendance_records (meeting_id, user_id, present, updated_at)
SELECT :meetingId, u, :present, NOW()
FROM unnest(:userIds) AS u
ON CONFLICT (meeting_id, user_id)
DO UPDATE SET present = EXCLUDED.present, updated_at = NOW()
INSERT INTO public.attendance_records (meeting_id, user_id, present, updated_at)
SELECT :meetingId, uid, :present, NOW()
FROM unnest(CAST(:userIds AS bigint[])) AS uid
ON CONFLICT (meeting_id, user_id)
DO UPDATE SET present = EXCLUDED.present, updated_at = NOW()
""", nativeQuery = true)
int upsertBatchByMeetingId(@Param("meetingId") Long meetingId, @Param("userIds") List<Long> userIds, @Param("present") boolean present);
int upsertBatchByMeetingId(@Param("meetingId") Long meetingId, @Param("userIds") Long[] userIds, // ๐Ÿ‘ˆ ๋ฐฐ์—ด
@Param("present") boolean present);
}
Loading
Loading