Compare commits

..

No commits in common. "main" and "feature/admin-and-api" have entirely different histories.

21개의 변경된 파일14개의 추가작업 그리고 562개의 파일을 삭제

파일 보기

@ -3,12 +3,9 @@ package com.gcsc.guide.auth;
import com.gcsc.guide.dto.AuthResponse; import com.gcsc.guide.dto.AuthResponse;
import com.gcsc.guide.dto.GoogleLoginRequest; import com.gcsc.guide.dto.GoogleLoginRequest;
import com.gcsc.guide.dto.UserResponse; import com.gcsc.guide.dto.UserResponse;
import com.gcsc.guide.entity.Role;
import com.gcsc.guide.entity.User; import com.gcsc.guide.entity.User;
import com.gcsc.guide.repository.RoleRepository;
import com.gcsc.guide.repository.UserRepository; import com.gcsc.guide.repository.UserRepository;
import com.gcsc.guide.service.ActivityService; import com.gcsc.guide.service.ActivityService;
import com.gcsc.guide.service.SettingsService;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Content;
@ -21,9 +18,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.HashSet;
import java.util.List;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -42,9 +36,7 @@ public class AuthController {
private final GoogleTokenVerifier googleTokenVerifier; private final GoogleTokenVerifier googleTokenVerifier;
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository; private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final ActivityService activityService; private final ActivityService activityService;
private final SettingsService settingsService;
@Operation(summary = "Google 로그인", @Operation(summary = "Google 로그인",
description = "Google ID Token을 검증하고 JWT를 발급합니다. " description = "Google ID Token을 검증하고 JWT를 발급합니다. "
@ -84,7 +76,7 @@ public class AuthController {
activityService.recordLogin( activityService.recordLogin(
userWithRoles.getId(), userWithRoles.getId(),
resolveClientIp(httpRequest), httpRequest.getRemoteAddr(),
httpRequest.getHeader("User-Agent")); httpRequest.getHeader("User-Agent"));
String token = jwtTokenProvider.generateToken( String token = jwtTokenProvider.generateToken(
@ -127,31 +119,9 @@ public class AuthController {
newUser.activate(); newUser.activate();
newUser.grantAdmin(); newUser.grantAdmin();
log.info("관리자 자동 승인: {}", email); log.info("관리자 자동 승인: {}", email);
} else if (settingsService.isAutoApproveEnabled()) {
newUser.activate();
log.info("자동 승인 (설정 활성화): {}", email);
}
List<Role> defaultRoles = roleRepository.findByDefaultGrantTrue();
if (!defaultRoles.isEmpty()) {
newUser.updateRoles(new HashSet<>(defaultRoles));
log.info("기본 롤 부여: {} → {}", email,
defaultRoles.stream().map(Role::getName).toList());
} }
newUser.updateLastLogin(); newUser.updateLastLogin();
return userRepository.save(newUser); return userRepository.save(newUser);
} }
private String resolveClientIp(HttpServletRequest request) {
String xff = request.getHeader("X-Forwarded-For");
if (xff != null && !xff.isBlank()) {
return xff.split(",")[0].trim();
}
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) {
return realIp.trim();
}
return request.getRemoteAddr();
}
} }

파일 보기

@ -8,9 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List;
@Slf4j @Slf4j
@Component @Component
@ -20,26 +18,12 @@ public class GoogleTokenVerifier {
private final String allowedEmailDomain; private final String allowedEmailDomain;
public GoogleTokenVerifier( public GoogleTokenVerifier(
@Value("${app.google.client-ids:}") String clientIdsCsv, @Value("${app.google.client-id}") String clientId,
@Value("${app.google.client-id:}") String clientId,
@Value("${app.allowed-email-domain}") String allowedEmailDomain @Value("${app.allowed-email-domain}") String allowedEmailDomain
) { ) {
List<String> audiences = new ArrayList<>();
if (clientIdsCsv != null && !clientIdsCsv.isBlank()) {
for (String part : clientIdsCsv.split(",")) {
String trimmed = part == null ? "" : part.trim();
if (!trimmed.isEmpty()) audiences.add(trimmed);
}
}
if (audiences.isEmpty() && clientId != null && !clientId.isBlank()) {
audiences.add(clientId.trim());
}
if (audiences.isEmpty()) {
log.warn("Google client id is not configured (app.google.client-id / app.google.client-ids empty). Google login will fail.");
}
this.verifier = new GoogleIdTokenVerifier.Builder( this.verifier = new GoogleIdTokenVerifier.Builder(
new NetHttpTransport(), GsonFactory.getDefaultInstance()) new NetHttpTransport(), GsonFactory.getDefaultInstance())
.setAudience(audiences.isEmpty() ? Collections.emptyList() : audiences) .setAudience(Collections.singletonList(clientId))
.build(); .build();
this.allowedEmailDomain = allowedEmailDomain; this.allowedEmailDomain = allowedEmailDomain;
} }

파일 보기

@ -29,7 +29,7 @@ public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
@Value("${app.cors.allowed-origins:http://localhost:5173,http://localhost:5175,http://127.0.0.1:5175,https://guide.gc-si.dev,https://wing.gc-si.dev}") @Value("${app.cors.allowed-origins:http://localhost:5173,https://guide.gc-si.dev}")
private List<String> allowedOrigins; private List<String> allowedOrigins;
@Bean @Bean
@ -41,8 +41,7 @@ public class SecurityConfig {
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers( .requestMatchers(
"/api/auth/google", "/api/auth/**",
"/api/auth/logout",
"/api/health", "/api/health",
"/actuator/health", "/actuator/health",
"/h2-console/**", "/h2-console/**",

파일 보기

@ -3,7 +3,6 @@ package com.gcsc.guide.controller;
import com.gcsc.guide.dto.AddPermissionRequest; import com.gcsc.guide.dto.AddPermissionRequest;
import com.gcsc.guide.dto.CreateRoleRequest; import com.gcsc.guide.dto.CreateRoleRequest;
import com.gcsc.guide.dto.RoleResponse; import com.gcsc.guide.dto.RoleResponse;
import com.gcsc.guide.dto.UpdateDefaultGrantRequest;
import com.gcsc.guide.service.RoleService; import com.gcsc.guide.service.RoleService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -120,22 +119,6 @@ public class AdminRoleController {
return ResponseEntity.ok(roleService.addPermission(id, request.urlPattern())); return ResponseEntity.ok(roleService.addPermission(id, request.urlPattern()));
} }
@Operation(summary = "롤 기본 부여 토글",
description = "신규 가입자에게 해당 롤을 기본 부여할지 설정합니다. defaultGrant=true인 롤은 첫 로그인 시 자동 할당됩니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "수정 성공",
content = @Content(schema = @Schema(implementation = RoleResponse.class))),
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
@ApiResponse(responseCode = "403", description = "관리자 권한 필요", content = @Content),
@ApiResponse(responseCode = "404", description = "롤을 찾을 수 없음", content = @Content)
})
@PutMapping("/{id}/default-grant")
public ResponseEntity<RoleResponse> updateDefaultGrant(
@Parameter(description = "롤 ID", required = true) @PathVariable Long id,
@Valid @RequestBody UpdateDefaultGrantRequest request) {
return ResponseEntity.ok(roleService.updateDefaultGrant(id, request.defaultGrant()));
}
@Operation(summary = "URL 패턴 삭제", @Operation(summary = "URL 패턴 삭제",
description = "특정 URL 패턴(권한)을 삭제합니다.") description = "특정 URL 패턴(권한)을 삭제합니다.")
@ApiResponses({ @ApiResponses({

파일 보기

@ -1,53 +0,0 @@
package com.gcsc.guide.controller;
import com.gcsc.guide.dto.RegistrationSettingsResponse;
import com.gcsc.guide.dto.UpdateRegistrationSettingsRequest;
import com.gcsc.guide.service.SettingsService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/admin/settings")
@RequiredArgsConstructor
@Tag(name = "07. 관리자 - 설정", description = "신규 가입 자동승인, 기본 롤 부여 등 시스템 설정 관리")
@SecurityRequirement(name = "Bearer JWT")
public class AdminSettingsController {
private final SettingsService settingsService;
@Operation(summary = "가입 설정 조회",
description = "자동 승인 여부와 기본 부여 롤 목록을 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "조회 성공",
content = @Content(schema = @Schema(implementation = RegistrationSettingsResponse.class))),
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
@ApiResponse(responseCode = "403", description = "관리자 권한 필요", content = @Content)
})
@GetMapping("/registration")
public ResponseEntity<RegistrationSettingsResponse> getRegistrationSettings() {
return ResponseEntity.ok(settingsService.getRegistrationSettings());
}
@Operation(summary = "가입 설정 수정",
description = "자동 승인 여부를 변경합니다. true로 설정하면 신규 가입자가 즉시 ACTIVE 상태가 됩니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "수정 성공",
content = @Content(schema = @Schema(implementation = RegistrationSettingsResponse.class))),
@ApiResponse(responseCode = "401", description = "인증 실패", content = @Content),
@ApiResponse(responseCode = "403", description = "관리자 권한 필요", content = @Content)
})
@PutMapping("/registration")
public ResponseEntity<RegistrationSettingsResponse> updateRegistrationSettings(
@Valid @RequestBody UpdateRegistrationSettingsRequest request) {
return ResponseEntity.ok(settingsService.updateAutoApprove(request.autoApprove()));
}
}

파일 보기

@ -1,215 +0,0 @@
package com.gcsc.guide.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/wing/ais-target")
@RequiredArgsConstructor
@Tag(name = "WING · AIS", description = "WING demo AIS proxy (JWT required)")
public class WingAisController {
private final ObjectMapper objectMapper;
@Value("${app.wing.ais.upstream-base:http://211.208.115.83:8041}")
private String upstreamBase;
@Value("${app.wing.ais.timeout-ms:20000}")
private long timeoutMs;
private HttpClient httpClient;
@PostConstruct
void initHttpClient() {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
}
private record Bbox(double lonMin, double latMin, double lonMax, double latMax) {
}
@GetMapping("/search")
public ResponseEntity<?> search(
@RequestParam(name = "minutes") String minutesRaw,
@RequestParam(name = "bbox", required = false) String bboxRaw,
@RequestParam(name = "centerLon", required = false) Double centerLon,
@RequestParam(name = "centerLat", required = false) Double centerLat,
@RequestParam(name = "radiusMeters", required = false) Double radiusMeters
) {
Integer minutes = parseMinutes(minutesRaw);
if (minutes == null) {
return error(HttpStatus.BAD_REQUEST, "invalid minutes", "BAD_REQUEST");
}
Bbox bbox = parseBbox(bboxRaw);
if (bboxRaw != null && bbox == null) {
return error(HttpStatus.BAD_REQUEST, "invalid bbox", "BAD_REQUEST");
}
URI upstreamUrl = buildUpstreamUrl(minutes, centerLon, centerLat, radiusMeters);
HttpRequest req = HttpRequest.newBuilder(upstreamUrl)
.timeout(Duration.ofMillis(timeoutMs))
.header("accept", "application/json")
.GET()
.build();
int status;
String body;
try {
HttpResponse<String> res = httpClient.send(req, HttpResponse.BodyHandlers.ofString());
status = res.statusCode();
body = res.body() == null ? "" : res.body();
} catch (java.net.http.HttpTimeoutException e) {
log.warn("AIS upstream timeout ({}ms): {}", timeoutMs, upstreamUrl);
return error(HttpStatus.GATEWAY_TIMEOUT, "upstream timeout (" + timeoutMs + "ms)", "UPSTREAM_TIMEOUT");
} catch (Exception e) {
log.warn("AIS upstream fetch failed: {} ({})", upstreamUrl, e.toString());
return error(HttpStatus.BAD_GATEWAY, "upstream fetch failed", "UPSTREAM_FETCH_FAILED");
}
if (status < 200 || status >= 300) {
log.warn("AIS upstream error: status={} url={}", status, upstreamUrl);
return error(HttpStatus.BAD_GATEWAY, "upstream error", "UPSTREAM");
}
// Fast path: no bbox requested, proxy raw payload.
if (bbox == null) {
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(body);
}
try {
Map<String, Object> json = objectMapper.readValue(body, new TypeReference<>() {
});
Object dataObj = json.get("data");
List<?> rows = dataObj instanceof List<?> l ? l : List.of();
List<Object> filtered = new ArrayList<>(rows.size());
for (Object row : rows) {
if (inBbox(row, bbox)) {
filtered.add(row);
}
}
json.put("data", filtered);
Object msgObj = json.get("message");
String msg = msgObj instanceof String s ? s : "";
String suffix = " (bbox: " + filtered.size() + "/" + rows.size() + ")";
json.put("message", (msg + suffix).trim());
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(json);
} catch (Exception e) {
log.warn("AIS upstream JSON parse/filter failed: {}", e.toString());
return error(HttpStatus.BAD_GATEWAY, "upstream invalid json", "UPSTREAM_INVALID_JSON");
}
}
private Integer parseMinutes(String raw) {
if (raw == null) return null;
int minutes;
try {
minutes = Integer.parseInt(raw);
} catch (NumberFormatException e) {
return null;
}
if (minutes <= 0 || minutes > 60 * 24) return null;
return minutes;
}
private Bbox parseBbox(String raw) {
if (raw == null || raw.isBlank()) return null;
String[] parts = raw.split(",");
if (parts.length != 4) return null;
Double lonMin = toDouble(parts[0]);
Double latMin = toDouble(parts[1]);
Double lonMax = toDouble(parts[2]);
Double latMax = toDouble(parts[3]);
if (lonMin == null || latMin == null || lonMax == null || latMax == null) return null;
boolean ok =
lonMin >= -180 && lonMax <= 180 &&
latMin >= -90 && latMax <= 90 &&
lonMin < lonMax &&
latMin < latMax;
if (!ok) return null;
return new Bbox(lonMin, latMin, lonMax, latMax);
}
private boolean inBbox(Object row, Bbox bbox) {
if (!(row instanceof Map<?, ?> m)) return false;
Double lon = toDouble(m.get("lon"));
Double lat = toDouble(m.get("lat"));
if (lon == null || lat == null) return false;
return lon >= bbox.lonMin && lon <= bbox.lonMax && lat >= bbox.latMin && lat <= bbox.latMax;
}
private Double toDouble(Object value) {
if (value == null) return null;
if (value instanceof Number n) return n.doubleValue();
if (value instanceof String s) {
String t = s.trim();
if (t.isEmpty()) return null;
try {
return Double.parseDouble(t);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
private URI buildUpstreamUrl(int minutes, Double centerLon, Double centerLat, Double radiusMeters) {
String base = upstreamBase == null ? "" : upstreamBase.trim();
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
StringBuilder sb = new StringBuilder(base);
sb.append("/snp-api/api/ais-target/search");
sb.append("?minutes=").append(minutes);
// Upstream supports center/radius filtering; bbox is ignored (filtered server-side here).
if (centerLon != null && Double.isFinite(centerLon)) sb.append("&centerLon=").append(centerLon);
if (centerLat != null && Double.isFinite(centerLat)) sb.append("&centerLat=").append(centerLat);
if (radiusMeters != null && Double.isFinite(radiusMeters)) sb.append("&radiusMeters=").append(radiusMeters);
return URI.create(sb.toString());
}
private ResponseEntity<Map<String, Object>> error(HttpStatus status, String message, String errorCode) {
return ResponseEntity.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(Map.of(
"success", false,
"message", message,
"data", List.of(),
"errorCode", errorCode
));
}
}

파일 보기

@ -1,57 +0,0 @@
package com.gcsc.guide.controller;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.http.CacheControl;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.concurrent.TimeUnit;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@RestController
@RequestMapping("/api/wing/data")
@RequiredArgsConstructor
@Tag(name = "WING · Data", description = "WING embedded datasets (JWT required)")
public class WingDataController {
@GetMapping(value = "/zones", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Resource> zones() {
return serveJson("wing-data/zones.wgs84.geojson");
}
@GetMapping(value = "/legacy", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Resource> legacyChinesePermitted() {
return serveJson("wing-data/chinese-permitted.v1.json");
}
@GetMapping(value = "/subcables/geo", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Resource> subcablesGeo() {
return serveJson("wing-data/subcables/cable-geo.json");
}
@GetMapping(value = "/subcables/details", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Resource> subcablesDetails() {
return serveJson("wing-data/subcables/cable-details.min.json");
}
private ResponseEntity<Resource> serveJson(String classpathLocation) {
Resource resource = new ClassPathResource(classpathLocation);
if (!resource.exists()) {
throw new ResponseStatusException(NOT_FOUND, "Resource not found: " + classpathLocation);
}
return ResponseEntity.ok()
// Authenticated endpoint: allow browser caching but keep it private.
.cacheControl(CacheControl.maxAge(6, TimeUnit.HOURS).cachePrivate())
.contentType(MediaType.APPLICATION_JSON)
.body(resource);
}
}

파일 보기

@ -1,8 +0,0 @@
package com.gcsc.guide.dto;
import java.util.List;
public record RegistrationSettingsResponse(
boolean autoApprove,
List<RoleResponse> defaultRoles
) {}

파일 보기

@ -9,8 +9,7 @@ public record RoleResponse(
Long id, Long id,
String name, String name,
String description, String description,
List<String> urlPatterns, List<String> urlPatterns
boolean defaultGrant
) { ) {
public static RoleResponse from(Role role) { public static RoleResponse from(Role role) {
@ -22,8 +21,7 @@ public record RoleResponse(
role.getId(), role.getId(),
role.getName(), role.getName(),
role.getDescription(), role.getDescription(),
patterns, patterns
role.isDefaultGrant()
); );
} }
} }

파일 보기

@ -1,7 +0,0 @@
package com.gcsc.guide.dto;
import jakarta.validation.constraints.NotNull;
public record UpdateDefaultGrantRequest(
@NotNull Boolean defaultGrant
) {}

파일 보기

@ -1,7 +0,0 @@
package com.gcsc.guide.dto;
import jakarta.validation.constraints.NotNull;
public record UpdateRegistrationSettingsRequest(
@NotNull Boolean autoApprove
) {}

파일 보기

@ -1,46 +0,0 @@
package com.gcsc.guide.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Table(name = "app_settings")
@Getter
@NoArgsConstructor
public class AppSetting {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "setting_key", nullable = false, unique = true, length = 100)
private String settingKey;
@Column(name = "setting_value", nullable = false, length = 500)
private String settingValue;
@Column(length = 255)
private String description;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
public AppSetting(String settingKey, String settingValue, String description) {
this.settingKey = settingKey;
this.settingValue = settingValue;
this.description = description;
}
public void updateValue(String value) {
this.settingValue = value;
this.updatedAt = LocalDateTime.now();
}
@PrePersist
protected void onCreate() {
this.updatedAt = LocalDateTime.now();
}
}

파일 보기

@ -24,9 +24,6 @@ public class Role {
@Column(length = 255) @Column(length = 255)
private String description; private String description;
@Column(name = "default_grant")
private boolean defaultGrant = false;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt; private LocalDateTime createdAt;
@ -43,10 +40,6 @@ public class Role {
this.description = description; this.description = description;
} }
public void updateDefaultGrant(boolean defaultGrant) {
this.defaultGrant = defaultGrant;
}
@PrePersist @PrePersist
protected void onCreate() { protected void onCreate() {
this.createdAt = LocalDateTime.now(); this.createdAt = LocalDateTime.now();

파일 보기

@ -1,11 +0,0 @@
package com.gcsc.guide.repository;
import com.gcsc.guide.entity.AppSetting;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface AppSettingRepository extends JpaRepository<AppSetting, Long> {
Optional<AppSetting> findBySettingKey(String settingKey);
}

파일 보기

@ -16,6 +16,4 @@ public interface RoleRepository extends JpaRepository<Role, Long> {
@Query("SELECT r FROM Role r LEFT JOIN FETCH r.urlPatterns WHERE r.id = :id") @Query("SELECT r FROM Role r LEFT JOIN FETCH r.urlPatterns WHERE r.id = :id")
Optional<Role> findByIdWithUrlPatterns(Long id); Optional<Role> findByIdWithUrlPatterns(Long id);
List<Role> findByDefaultGrantTrue();
} }

파일 보기

@ -81,14 +81,6 @@ public class RoleService {
roleUrlPatternRepository.deleteById(permissionId); roleUrlPatternRepository.deleteById(permissionId);
} }
@Transactional
public RoleResponse updateDefaultGrant(Long roleId, boolean defaultGrant) {
Role role = roleRepository.findByIdWithUrlPatterns(roleId)
.orElseThrow(() -> new ResourceNotFoundException("", roleId));
role.updateDefaultGrant(defaultGrant);
return RoleResponse.from(roleRepository.save(role));
}
private Role findRoleById(Long roleId) { private Role findRoleById(Long roleId) {
return roleRepository.findById(roleId) return roleRepository.findById(roleId)
.orElseThrow(() -> new ResourceNotFoundException("", roleId)); .orElseThrow(() -> new ResourceNotFoundException("", roleId));

파일 보기

@ -1,47 +0,0 @@
package com.gcsc.guide.service;
import com.gcsc.guide.dto.RegistrationSettingsResponse;
import com.gcsc.guide.dto.RoleResponse;
import com.gcsc.guide.entity.AppSetting;
import com.gcsc.guide.repository.AppSettingRepository;
import com.gcsc.guide.repository.RoleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SettingsService {
private static final String AUTO_APPROVE_KEY = "registration.auto-approve";
private final AppSettingRepository appSettingRepository;
private final RoleRepository roleRepository;
@Transactional(readOnly = true)
public boolean isAutoApproveEnabled() {
return appSettingRepository.findBySettingKey(AUTO_APPROVE_KEY)
.map(s -> Boolean.parseBoolean(s.getSettingValue()))
.orElse(false);
}
@Transactional(readOnly = true)
public RegistrationSettingsResponse getRegistrationSettings() {
boolean autoApprove = isAutoApproveEnabled();
List<RoleResponse> defaultRoles = roleRepository.findByDefaultGrantTrue().stream()
.map(RoleResponse::from)
.toList();
return new RegistrationSettingsResponse(autoApprove, defaultRoles);
}
@Transactional
public RegistrationSettingsResponse updateAutoApprove(boolean autoApprove) {
AppSetting setting = appSettingRepository.findBySettingKey(AUTO_APPROVE_KEY)
.orElseGet(() -> new AppSetting(AUTO_APPROVE_KEY, "false", "신규 가입자 자동 승인 여부"));
setting.updateValue(String.valueOf(autoApprove));
appSettingRepository.save(setting);
return getRegistrationSettings();
}
}

파일 보기

@ -27,15 +27,9 @@ app:
expiration-ms: ${JWT_EXPIRATION:86400000} # 24시간 expiration-ms: ${JWT_EXPIRATION:86400000} # 24시간
google: google:
client-id: ${GOOGLE_CLIENT_ID:} client-id: ${GOOGLE_CLIENT_ID:}
# Optional: allow multiple audiences (comma-separated) for shared auth across multiple frontends. allowed-email-domain: gcsc.co.kr
client-ids: ${GOOGLE_CLIENT_IDS:${GOOGLE_CLIENT_ID:}}
allowed-email-domain: ${ALLOWED_EMAIL_DOMAIN:gcsc.co.kr}
wing:
ais:
upstream-base: ${WING_AIS_UPSTREAM_BASE:http://211.208.115.83:8041}
timeout-ms: ${WING_AIS_TIMEOUT_MS:20000}
cors: cors:
allowed-origins: ${CORS_ORIGINS:http://localhost:5173,http://localhost:5175,http://127.0.0.1:5175,https://guide.gc-si.dev,https://wing.gc-si.dev} allowed-origins: ${CORS_ORIGINS:http://localhost:5173,https://guide.gc-si.dev}
# SpringDoc / Swagger # SpringDoc / Swagger
springdoc: springdoc:

파일 보기

@ -1,17 +1,11 @@
-- 초기 롤 시드 데이터 -- 초기 롤 시드 데이터
INSERT INTO roles (name, description, default_grant, created_at) VALUES INSERT INTO roles (name, description, created_at) VALUES
('ADMIN', '전체 접근 권한 (관리자 페이지 포함)', false, NOW()), ('ADMIN', '전체 접근 권한 (관리자 페이지 포함)', NOW()),
('DEVELOPER', '전체 개발 가이드 접근', false, NOW()), ('DEVELOPER', '전체 개발 가이드 접근', NOW()),
('FRONT_DEV', '프론트엔드 개발 가이드만', false, NOW()), ('FRONT_DEV', '프론트엔드 개발 가이드만', NOW());
('WING_PERMIT', 'Wing 데모 사이트 접근 권한', true, NOW());
-- 롤별 URL 패턴 -- 롤별 URL 패턴
INSERT INTO role_url_patterns (role_id, url_pattern, created_at) VALUES INSERT INTO role_url_patterns (role_id, url_pattern, created_at) VALUES
((SELECT id FROM roles WHERE name = 'ADMIN'), '/**', NOW()), ((SELECT id FROM roles WHERE name = 'ADMIN'), '/**', NOW()),
((SELECT id FROM roles WHERE name = 'DEVELOPER'), '/dev/**', NOW()), ((SELECT id FROM roles WHERE name = 'DEVELOPER'), '/dev/**', NOW()),
((SELECT id FROM roles WHERE name = 'FRONT_DEV'), '/dev/front/**', NOW()), ((SELECT id FROM roles WHERE name = 'FRONT_DEV'), '/dev/front/**', NOW());
((SELECT id FROM roles WHERE name = 'WING_PERMIT'), '/wing/**', NOW());
-- 시스템 설정
INSERT INTO app_settings (setting_key, setting_value, description, updated_at) VALUES
('registration.auto-approve', 'true', '신규 가입자 자동 승인 여부', NOW());

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long