Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion .github/workflows/deploy-to-dev-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ jobs:
ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" \
GOOGLE_GENAI_API_KEY="${{ secrets.GOOGLE_GENAI_API_KEY }}" \
FIREBASE_ADMIN_KEY="${{ secrets.FIREBASE_ADMIN_KEY }}" \
ADMIN_PAGE_PASSWORD="${{ secrets.ADMIN_PAGE_PASSWORD }}" \
nohup java -jar "$JAR_PATH" \
--spring.profiles.active=dev > app.log 2>&1 &

echo "✅ 배포 완료!"
echo "✅ 배포 완료!"
1 change: 1 addition & 0 deletions .github/workflows/deploy-to-prod-ec2.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ jobs:
ANTHROPIC_API_KEY="${{ secrets.ANTHROPIC_API_KEY }}" \
GOOGLE_GENAI_API_KEY="${{ secrets.GOOGLE_GENAI_API_KEY }}" \
FIREBASE_ADMIN_KEY="${{ secrets.FIREBASE_ADMIN_KEY }}" \
ADMIN_PAGE_PASSWORD="${{ secrets.ADMIN_PAGE_PASSWORD }}" \
nohup java -jar "$JAR_PATH" \
--spring.profiles.active=prod > app-prod.log 2>&1 &

Expand Down
4 changes: 4 additions & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ module.exports = {
"revert"
]],
"scope-enum": [2, "always", [
"admin",
"auth",
"user",
"ai",
Expand Down Expand Up @@ -100,6 +101,9 @@ module.exports = {
scope: {
description: '[Scope] 이번 변경이 적용된 범위를 선택해주세요 (범위 생략하려면 empty 선택)',
enum: {
admin: {
description: '🧑‍💻 어드민 도메인 (예: 어드민 페이지)'
},
auth: {
description: '🔐 인증/인가 도메인 (예: OAuth2, JWT, 세션)'
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.devkor.ifive.nadab.domain.admin.api;

import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminLoginRequest;
import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminAuthStatusResponse;
import com.devkor.ifive.nadab.domain.admin.application.AdminPageAuthCommandService;
import com.devkor.ifive.nadab.domain.admin.infra.security.AdminPageAuthCookieService;
import com.devkor.ifive.nadab.domain.admin.infra.security.AdminPageAuthTokenService;
import com.devkor.ifive.nadab.global.core.response.ApiResponseDto;
import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.annotation.security.PermitAll;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
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.RestController;

@Hidden
@RestController
@RequestMapping("/admin/api")
@RequiredArgsConstructor
public class AdminAuthController {

private final AdminPageAuthCommandService adminPageAuthCommandService;
private final AdminPageAuthTokenService adminPageAuthTokenService;
private final AdminPageAuthCookieService adminPageAuthCookieService;

@PostMapping("/login")
@PermitAll
public ResponseEntity<ApiResponseDto<Void>> login(
@RequestBody @Valid AdminLoginRequest request,
HttpServletResponse response
) {
adminPageAuthCommandService.validatePassword(request.password());
String token = adminPageAuthTokenService.issueToken();
adminPageAuthCookieService.addCookie(response, token);
return ApiResponseEntity.noContent();
}

@PostMapping("/logout")
public ResponseEntity<ApiResponseDto<Void>> logout(HttpServletResponse response) {
adminPageAuthCookieService.expireCookie(response);
return ApiResponseEntity.noContent();
}

@GetMapping("/auth-status")
public ResponseEntity<ApiResponseDto<AdminAuthStatusResponse>> authStatus() {
return ApiResponseEntity.ok(new AdminAuthStatusResponse(true));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.devkor.ifive.nadab.domain.admin.api;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class AdminPageController {

@GetMapping("/admin/login")
public String loginPage() {
return "admin/login";
}

@GetMapping("/admin")
public String adminRoot() {
return "redirect:/admin/tabs/app-versions";
}

@GetMapping("/admin/tabs/app-versions")
public String adminVersionPage() {
return "admin/version";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.devkor.ifive.nadab.domain.admin.api;

import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionCreateRequest;
import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionItemUpsertRequest;
import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionSummaryUpdateRequest;
import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminLatestVersionsResponse;
import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionCreateResponse;
import com.devkor.ifive.nadab.domain.admin.api.dto.response.AdminVersionItemCreateResponse;
import com.devkor.ifive.nadab.domain.admin.application.AdminVersionCommandService;
import com.devkor.ifive.nadab.domain.admin.application.AdminVersionItemCommandService;
import com.devkor.ifive.nadab.domain.admin.application.AdminVersionQueryService;
import com.devkor.ifive.nadab.global.core.response.ApiResponseDto;
import com.devkor.ifive.nadab.global.core.response.ApiResponseEntity;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Hidden
@RestController
@RequestMapping("/admin/api/versions")
@RequiredArgsConstructor
public class AdminVersionController {

private final AdminVersionQueryService adminVersionQueryService;
private final AdminVersionCommandService adminVersionCommandService;
private final AdminVersionItemCommandService adminVersionItemCommandService;

@GetMapping("/latest")
public ResponseEntity<ApiResponseDto<AdminLatestVersionsResponse>> getLatestVersions() {
return ApiResponseEntity.ok(adminVersionQueryService.getLatestVersions());
}

@PostMapping
public ResponseEntity<ApiResponseDto<AdminVersionCreateResponse>> createVersion(
@RequestBody @Valid AdminVersionCreateRequest request
) {
Long appVersionId = adminVersionCommandService.createVersion(request);
return ApiResponseEntity.created(new AdminVersionCreateResponse(appVersionId));
}

@PutMapping("/{appVersionId}/summary")
public ResponseEntity<ApiResponseDto<Void>> updateVersionSummary(
@PathVariable Long appVersionId,
@RequestBody @Valid AdminVersionSummaryUpdateRequest request
) {
adminVersionCommandService.updateSummary(appVersionId, request.summary());
return ApiResponseEntity.noContent();
}

@PostMapping("/{appVersionId}/items")
public ResponseEntity<ApiResponseDto<AdminVersionItemCreateResponse>> createVersionItem(
@PathVariable Long appVersionId,
@RequestBody @Valid AdminVersionItemUpsertRequest request
) {
Long appVersionItemId = adminVersionItemCommandService.createItem(appVersionId, request);
return ApiResponseEntity.created(new AdminVersionItemCreateResponse(appVersionItemId));
}

@PutMapping("/items/{appVersionItemId}")
public ResponseEntity<ApiResponseDto<Void>> updateVersionItem(
@PathVariable Long appVersionItemId,
@RequestBody @Valid AdminVersionItemUpsertRequest request
) {
adminVersionItemCommandService.updateItem(appVersionItemId, request);
return ApiResponseEntity.noContent();
}

@DeleteMapping("/items/{appVersionItemId}")
public ResponseEntity<ApiResponseDto<Void>> deleteVersionItem(
@PathVariable Long appVersionItemId
) {
adminVersionItemCommandService.deleteItem(appVersionItemId);
return ApiResponseEntity.noContent();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.request;

import jakarta.validation.constraints.NotBlank;

public record AdminLoginRequest(
@NotBlank(message = "비밀번호는 필수입니다.")
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.request;

import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record AdminVersionCreateRequest(
@NotNull(message = "플랫폼은 필수입니다.")
AppPlatform platform,

@NotBlank(message = "버전은 필수입니다.")
@Size(max = 30, message = "버전은 30자 이하여야 합니다.")
String version,

@NotNull(message = "요약은 null일 수 없습니다.")
@Size(max = 120, message = "요약은 120자 이하여야 합니다.")
String summary
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record AdminVersionItemUpsertRequest(
@NotBlank(message = "업데이트명은 필수입니다.")
@Size(max = 100, message = "업데이트명은 100자 이하여야 합니다.")
String title,

@NotBlank(message = "상세 내용은 필수입니다.")
@Size(max = 500, message = "상세 내용은 500자 이하여야 합니다.")
String description,

@NotNull(message = "displayOrder는 필수입니다.")
@Min(value = 1, message = "displayOrder는 1 이상이어야 합니다.")
@Max(value = 9999, message = "displayOrder는 9999 이하여야 합니다.")
Integer displayOrder
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.request;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public record AdminVersionSummaryUpdateRequest(
@NotNull(message = "요약은 null일 수 없습니다.")
@Size(max = 120, message = "요약은 120자 이하여야 합니다.")
String summary
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

public record AdminAuthStatusResponse(
boolean authenticated
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

import java.util.List;

public record AdminLatestVersionsResponse(
List<AdminVersionResponse> versions
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

public record AdminVersionCreateResponse(
Long appVersionId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

public record AdminVersionItemCreateResponse(
Long appVersionItemId
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

public record AdminVersionItemResponse(
Long id,
String title,
String description,
Integer displayOrder
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.devkor.ifive.nadab.domain.admin.api.dto.response;

import com.devkor.ifive.nadab.domain.appversion.core.entity.AppPlatform;

import java.util.List;

public record AdminVersionResponse(
Long id,
AppPlatform platform,
String version,
String summary,
List<AdminVersionItemResponse> items
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.devkor.ifive.nadab.domain.admin.application;

import com.devkor.ifive.nadab.domain.admin.core.properties.AdminPageProperties;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.UnauthorizedException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@Service
@RequiredArgsConstructor
public class AdminPageAuthCommandService {

private final AdminPageProperties adminPageProperties;

public void validatePassword(String rawPassword) {
byte[] input = rawPassword.getBytes(StandardCharsets.UTF_8);
byte[] expected = adminPageProperties.getPassword().getBytes(StandardCharsets.UTF_8);

if (!MessageDigest.isEqual(input, expected)) {
throw new UnauthorizedException(ErrorCode.ADMIN_PAGE_INVALID_PASSWORD);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.devkor.ifive.nadab.domain.admin.application;

import com.devkor.ifive.nadab.domain.admin.api.dto.request.AdminVersionCreateRequest;
import com.devkor.ifive.nadab.domain.appversion.core.entity.AppVersion;
import com.devkor.ifive.nadab.domain.appversion.core.repository.AppVersionRepository;
import com.devkor.ifive.nadab.global.core.response.ErrorCode;
import com.devkor.ifive.nadab.global.exception.ConflictException;
import com.devkor.ifive.nadab.global.exception.NotFoundException;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class AdminVersionCommandService {

private final AppVersionRepository appVersionRepository;

public Long createVersion(AdminVersionCreateRequest request) {
if (appVersionRepository.existsByPlatformAndVersion(request.platform(), request.version())) {
throw new ConflictException(ErrorCode.APP_VERSION_ALREADY_EXISTS);
}

appVersionRepository.findByPlatformAndIsLatestTrue(request.platform())
.ifPresent(AppVersion::markAsNotLatest);
appVersionRepository.flush();

AppVersion appVersion = AppVersion.create(
request.platform(),
request.version(),
request.summary()
);
try {
appVersionRepository.saveAndFlush(appVersion);
return appVersion.getId();
} catch (DataIntegrityViolationException e) {
throw new ConflictException(ErrorCode.APP_VERSION_ALREADY_EXISTS);
}
}

public void updateSummary(Long appVersionId, String summary) {
AppVersion appVersion = appVersionRepository.findById(appVersionId)
.orElseThrow(() -> new NotFoundException(ErrorCode.APP_VERSION_NOT_FOUND));
appVersion.updateSummary(summary);
}
}
Loading
Loading