[Developer] #424 fix Oracle DATE token errors

Refs #424
This commit is contained in:
devmrko
2026-06-23 11:43:51 +09:00
parent 9e812042b6
commit a6dcaa1b63
9 changed files with 118 additions and 29 deletions

View File

@@ -1,6 +1,6 @@
package com.cloudhandson.vpdbackoffice.domain.token;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
public record BearerTokenRecord(
long keyId,
@@ -8,12 +8,12 @@ public record BearerTokenRecord(
String username,
String keyPrefix,
String keyHash,
OffsetDateTime expiresAt,
OffsetDateTime revokedAt,
LocalDateTime expiresAt,
LocalDateTime revokedAt,
String description
) {
public boolean active(OffsetDateTime now) {
public boolean active(LocalDateTime now) {
return revokedAt == null && expiresAt != null && expiresAt.isAfter(now);
}
}

View File

@@ -1,7 +1,7 @@
package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -19,7 +19,7 @@ public interface BearerTokenMapper {
int revokeToken(
@Param("keyId") long keyId,
@Param("revokedAt") OffsetDateTime revokedAt,
@Param("revokedAt") LocalDateTime revokedAt,
@Param("reason") String reason
);
}

View File

@@ -9,7 +9,9 @@ import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
import com.cloudhandson.vpdbackoffice.mapper.BearerTokenMapper;
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -81,6 +83,9 @@ public class BearerTokenService {
String prefix = tokenGenerator.prefix(plainToken);
String hash = tokenHasher.sha256(plainToken);
long keyId = tokenMapper.nextKeyId();
LocalDateTime expiresAt = command.expiresAt()
.atZoneSameInstant(ZoneId.systemDefault())
.toLocalDateTime();
tokenMapper.insertToken(new BearerTokenRecord(
keyId,
@@ -88,7 +93,7 @@ public class BearerTokenService {
user.username(),
prefix,
hash,
command.expiresAt(),
expiresAt,
null,
command.description()
));
@@ -98,7 +103,11 @@ public class BearerTokenService {
@Transactional
public void revokeToken(long keyId, String reason) {
int updated = tokenMapper.revokeToken(keyId, OffsetDateTime.now(clock), reason);
int updated = tokenMapper.revokeToken(
keyId,
LocalDateTime.now(clock.withZone(ZoneId.systemDefault())),
reason
);
if (updated == 0) {
throw new AppException("회수할 활성 토큰을 찾을 수 없습니다.");
}

View File

@@ -13,7 +13,8 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.time.Clock;
import java.time.OffsetDateTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
@@ -71,7 +72,7 @@ public class OrdsProbeService {
return auditAndReturn(command, ProbeResult.blocked(
ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다."));
}
if (!token.active(OffsetDateTime.now(clock))) {
if (!token.active(LocalDateTime.now(clock.withZone(ZoneId.systemDefault())))) {
return auditAndReturn(command, ProbeResult.blocked(
ProbeStatus.TOKEN_INACTIVE, "TOKEN_INACTIVE", "만료되었거나 회수된 토큰입니다."));
}

View File

@@ -17,13 +17,17 @@ public class AppExceptionHandler {
@ExceptionHandler(DataAccessException.class)
public String handleDataAccessException(DataAccessException exception, Model model) {
String detail = exception.getMostSpecificCause() == null
? exception.getMessage()
: exception.getMostSpecificCause().getMessage();
model.addAttribute("errorTitle", "ADB 연결 설정이 필요합니다.");
model.addAttribute("errorMessage",
"BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, BACKOFFICE_DB_PASSWORD를 실제 ADB 값으로 설정하고 "
+ "./run.sh backoffice-support를 실행한 뒤 다시 시도하세요. 상세: " + detail);
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
model.addAttribute("errorTitle", error.title());
model.addAttribute("errorMessage", error.message());
return "error";
}
@ExceptionHandler(Exception.class)
public String handleUnexpectedException(Exception exception, Model model) {
RuntimeErrorMessage error = RuntimeErrorMessages.unexpected(exception);
model.addAttribute("errorTitle", error.title());
model.addAttribute("errorMessage", error.message());
return "error";
}
}

View File

@@ -33,18 +33,14 @@ public class DashboardController {
model.addAttribute("roles", permissionService.findRoles());
model.addAttribute("tokens", bearerTokenService.findAll());
} catch (DataAccessException e) {
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(e);
model.addAttribute("objects", List.of());
model.addAttribute("roles", List.of());
model.addAttribute("tokens", List.of());
model.addAttribute("setupError", dbSetupMessage(e));
model.addAttribute("runtimeErrorTitle", error.title());
model.addAttribute("runtimeErrorMessage", error.message());
model.addAttribute("showSupportCommand", error.showSupportCommand());
}
return "dashboard";
}
private String dbSetupMessage(DataAccessException e) {
String detail = e.getMostSpecificCause() == null ? e.getMessage() : e.getMostSpecificCause().getMessage();
return "ADB 연결을 확인할 수 없습니다. BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, "
+ "BACKOFFICE_DB_PASSWORD를 설정하고 ./run.sh backoffice-support를 먼저 실행하세요. 상세: "
+ detail;
}
}

View File

@@ -0,0 +1,8 @@
package com.cloudhandson.vpdbackoffice.web;
public record RuntimeErrorMessage(
String title,
String message,
boolean showSupportCommand
) {
}

View File

@@ -0,0 +1,71 @@
package com.cloudhandson.vpdbackoffice.web;
import java.sql.SQLException;
import java.util.Locale;
import org.springframework.dao.DataAccessException;
final class RuntimeErrorMessages {
private RuntimeErrorMessages() {
}
static RuntimeErrorMessage dataAccess(DataAccessException exception) {
String detail = detail(exception);
if (looksLikeConnectionProblem(exception, detail)) {
return new RuntimeErrorMessage(
"DB 연결 설정이 필요합니다.",
"ADB에 연결할 수 없습니다. BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, "
+ "BACKOFFICE_DB_PASSWORD를 확인하고 ./run.sh backoffice-support를 다시 실행하세요. 상세: "
+ detail,
true
);
}
return new RuntimeErrorMessage(
"데이터 처리 오류가 발생했습니다.",
"요청을 처리하는 중 DB 데이터 타입, SQL, 또는 매핑 오류가 발생했습니다. "
+ "입력값과 백오피스 테이블 스키마를 확인하세요. 상세: " + detail,
false
);
}
static RuntimeErrorMessage unexpected(Exception exception) {
return new RuntimeErrorMessage(
"요청 처리 중 오류가 발생했습니다.",
"예상하지 못한 오류가 발생했습니다. 입력값을 확인한 뒤 다시 시도하세요. 상세: "
+ safeMessage(exception),
false
);
}
private static boolean looksLikeConnectionProblem(DataAccessException exception, String detail) {
String text = (exception.getMessage() + " " + detail).toLowerCase(Locale.ROOT);
return text.contains("connection refused")
|| text.contains("the network adapter could not establish the connection")
|| text.contains("io error")
|| text.contains("ora-01017")
|| text.contains("ora-12154")
|| text.contains("ora-12514")
|| text.contains("ora-12541");
}
private static String detail(DataAccessException exception) {
Throwable cause = exception.getMostSpecificCause();
if (cause instanceof SQLException sqlException) {
return trim(sqlException.getMessage());
}
return trim(cause == null ? exception.getMessage() : safeMessage(cause));
}
private static String safeMessage(Throwable throwable) {
String message = throwable.getMessage();
return trim(message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message);
}
private static String trim(String value) {
if (value == null || value.isBlank()) {
return "상세 메시지가 없습니다.";
}
String normalized = value.replaceAll("\\s+", " ").trim();
return normalized.length() <= 300 ? normalized : normalized.substring(0, 300) + "...";
}
}

View File

@@ -9,10 +9,10 @@
<p>보호 객체, Bearer Token, ORDS 검증 상태를 확인합니다.</p>
</div>
<div class="alert alert-warning" th:if="${setupError}">
<div class="fw-semibold">DB 연결 설정이 필요합니다.</div>
<div th:text="${setupError}"></div>
<div class="mt-2">
<div class="alert alert-warning" th:if="${runtimeErrorMessage}">
<div class="fw-semibold" th:text="${runtimeErrorTitle}">데이터 처리 오류가 발생했습니다.</div>
<div th:text="${runtimeErrorMessage}"></div>
<div class="mt-2" th:if="${showSupportCommand}">
<code>./run.sh backoffice-support</code>
</div>
</div>