Compare commits
14 Commits
develop
...
02f9d41334
| Author | SHA1 | Date | |
|---|---|---|---|
| 02f9d41334 | |||
| fe4b176512 | |||
| 17c27e4398 | |||
| 10ff5d6f2f | |||
| f671f491b7 | |||
| 3d68589ae7 | |||
| 7268aef211 | |||
| 85b2f01ce0 | |||
| 0f255d7337 | |||
| a8323e78f5 | |||
| fa8fce4f2d | |||
| 4e7044b9fe | |||
| bddd7e3f85 | |||
| 71f4cb6f79 |
@@ -0,0 +1,217 @@
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
JAVA_HOME = '/apps/opts/jdk8'
|
||||
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
NODE_HOME = '/apps/opts/node-v24'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
CATALINA_BASE = '/prod/eapim/devportal'
|
||||
CATALINA_HOME = '/prod/eapim/apache-tomcat-9.0.116'
|
||||
CATALINA_PID = '/prod/eapim/devportal/temp/catalina.pid'
|
||||
DEPLOY_HTTP_PORT = '39130'
|
||||
WEBHOOK_URL = 'http://172.30.1.50:18000/api/hooks/01ba51b01d3c4c98ae8f3183a23a84ed713197857d024b5da4f303458195eb32'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
if [ ! -d elink-portal-common/.git ]; then
|
||||
rm -rf elink-portal-common
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
else
|
||||
git -C elink-portal-common fetch --depth=1 origin master
|
||||
git -C elink-portal-common reset --hard origin/master
|
||||
git -C elink-portal-common clean -fdx
|
||||
fi
|
||||
|
||||
mkdir -p eapim-online
|
||||
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||
rm -rf eapim-online/elink-online-core-jpa
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
else
|
||||
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
java -version
|
||||
gradle --version
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
steps {
|
||||
sh 'gradle clean test --no-daemon'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build WAR') {
|
||||
steps {
|
||||
sh 'gradle build -x test --no-daemon'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set -eu
|
||||
cd build/libs
|
||||
sha1sum eapim-portal.war eapim-portal-boot.war > SHA1SUMS
|
||||
sha256sum eapim-portal.war eapim-portal-boot.war > SHA256SUMS
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal-boot.war,build/libs/SHA1SUMS,build/libs/SHA256SUMS', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Stop Tomcat') {
|
||||
steps {
|
||||
sh '''
|
||||
set +e
|
||||
systemctl --user stop eapim-portal 2>/dev/null
|
||||
|
||||
if [ -f "$CATALINA_PID" ] && kill -0 "$(cat "$CATALINA_PID")" 2>/dev/null; then
|
||||
JAVA_HOME="$JAVA_HOME_TOMCAT" "$CATALINA_HOME/bin/shutdown.sh" 30 -force || true
|
||||
for i in $(seq 1 30); do
|
||||
[ -f "$CATALINA_PID" ] && kill -0 "$(cat "$CATALINA_PID")" 2>/dev/null || break
|
||||
sleep 1
|
||||
done
|
||||
fi
|
||||
rm -f "$CATALINA_PID"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy ROOT.war') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
DEPLOY_DIR="$CATALINA_BASE/webapps"
|
||||
rm -rf "$DEPLOY_DIR/ROOT" "$DEPLOY_DIR/ROOT.war"
|
||||
cp build/libs/eapim-portal.war "$DEPLOY_DIR/ROOT.war"
|
||||
ls -la "$DEPLOY_DIR"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Start Tomcat and readiness') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
LOG="$CATALINA_BASE/logs/catalina.$(date +%Y-%m-%d).log"
|
||||
BEFORE=0
|
||||
[ -f "$LOG" ] && BEFORE=$(stat -c %s "$LOG")
|
||||
|
||||
systemctl --user start eapim-portal
|
||||
|
||||
DEADLINE=$(($(date +%s) + 120))
|
||||
LIVE_OK=0
|
||||
STATUS=000
|
||||
|
||||
while [ "$(date +%s)" -lt "$DEADLINE" ]; do
|
||||
if [ "$LIVE_OK" = "0" ]; then
|
||||
LIVE=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
"http://localhost:$DEPLOY_HTTP_PORT/health" 2>/dev/null || echo "000")
|
||||
if [ "$LIVE" = "200" ]; then
|
||||
echo "Liveness OK (/health)"
|
||||
LIVE_OK=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$LIVE_OK" = "1" ]; then
|
||||
STATUS=$(curl -sS -o /tmp/eapim-ready-body.json -w '%{http_code}' --max-time 3 \
|
||||
"http://localhost:$DEPLOY_HTTP_PORT/health/ready" 2>/dev/null || echo "000")
|
||||
if [ "$STATUS" = "200" ]; then
|
||||
echo "Readiness OK (/health/ready)"
|
||||
cat /tmp/eapim-ready-body.json
|
||||
echo
|
||||
break
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | grep -qE "Application listener .* Exception|SEVERE.*startup.Catalina"; then
|
||||
echo "Startup error detected in log"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | tail -80
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
if [ "$STATUS" != "200" ]; then
|
||||
echo "Readiness probe failed within 120s, last HTTP status: $STATUS"
|
||||
[ -f /tmp/eapim-ready-body.json ] && cat /tmp/eapim-ready-body.json && echo
|
||||
[ -f "$LOG" ] && tail -c +$((BEFORE+1)) "$LOG" | tail -100
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- key boot log lines ---"
|
||||
tail -c +$((BEFORE+1)) "$LOG" | grep -E "Server startup|Deployment of web" | head -10 || true
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set +e
|
||||
payload=$(printf '{"text":"[%s #%s](%s) SUCCESS","attachments":[{"color":"good","fields":[{"title":"Job","value":"%s","short":true},{"title":"Build","value":"%s","short":true},{"title":"Result","value":"SUCCESS","short":true}]}]}' \
|
||||
"$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL" "$JOB_NAME" "$BUILD_NUMBER")
|
||||
curl -sS --fail --max-time 5 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST \
|
||||
--data "$payload" \
|
||||
"$WEBHOOK_URL" >/dev/null || echo "Webhook delivery failed for SUCCESS"
|
||||
'''
|
||||
}
|
||||
failure {
|
||||
sh '''
|
||||
set +e
|
||||
payload=$(printf '{"text":"[%s #%s](%s) FAILURE","attachments":[{"color":"danger","fields":[{"title":"Job","value":"%s","short":true},{"title":"Build","value":"%s","short":true},{"title":"Result","value":"FAILURE","short":true}]}]}' \
|
||||
"$JOB_NAME" "$BUILD_NUMBER" "$BUILD_URL" "$JOB_NAME" "$BUILD_NUMBER")
|
||||
curl -sS --fail --max-time 5 \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST \
|
||||
--data "$payload" \
|
||||
"$WEBHOOK_URL" >/dev/null || echo "Webhook delivery failed for FAILURE"
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
|
||||
triggers {
|
||||
pollSCM('H/10 * * * *')
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '20'))
|
||||
}
|
||||
|
||||
environment {
|
||||
JAVA_HOME = '/apps/opts/jdk8'
|
||||
JAVA_HOME_TOMCAT = '/apps/opts/jdk17'
|
||||
GRADLE_HOME = '/apps/opts/gradle-8.7'
|
||||
GRADLE_USER_HOME = '/apps/opts/gradle-home'
|
||||
NODE_HOME = '/apps/opts/node-v24'
|
||||
PATH = "/apps/opts/jdk8/bin:/apps/opts/gradle-8.7/bin:/apps/opts/node-v24/bin:/apps/opts/bin:${env.PATH}"
|
||||
GIT_SSH_COMMAND = 'ssh -o StrictHostKeyChecking=accept-new'
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout dependencies') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
cd "$WORKSPACE/.."
|
||||
|
||||
if [ ! -d elink-portal-common/.git ]; then
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-portal-common.git \
|
||||
elink-portal-common
|
||||
else
|
||||
git -C elink-portal-common fetch --depth=1 origin master
|
||||
git -C elink-portal-common reset --hard origin/master
|
||||
git -C elink-portal-common clean -fdx
|
||||
fi
|
||||
|
||||
mkdir -p eapim-online
|
||||
if [ ! -d eapim-online/elink-online-core-jpa/.git ]; then
|
||||
git clone --depth=1 --branch master \
|
||||
ssh://git@172.30.1.50:2222/djb-eapim/elink-online-core-jpa.git \
|
||||
eapim-online/elink-online-core-jpa
|
||||
else
|
||||
git -C eapim-online/elink-online-core-jpa fetch --depth=1 origin master
|
||||
git -C eapim-online/elink-online-core-jpa reset --hard origin/master
|
||||
git -C eapim-online/elink-online-core-jpa clean -fdx
|
||||
fi
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Verify toolchain') {
|
||||
steps {
|
||||
sh '''
|
||||
set -eu
|
||||
java -version
|
||||
gradle --version
|
||||
node --version || true
|
||||
npm --version || true
|
||||
'''
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
steps {
|
||||
sh 'gradle clean test --no-daemon'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
junit allowEmptyResults: true, testResults: 'build/test-results/test/*.xml'
|
||||
archiveArtifacts allowEmptyArchive: true, artifacts: 'build/reports/tests/test/**,build/test-results/test/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build WAR') {
|
||||
steps {
|
||||
sh 'gradle build -x test --no-daemon'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
sh '''
|
||||
set -eu
|
||||
cd build/libs
|
||||
sha1sum eapim-portal.war eapim-portal-boot.war > SHA1SUMS
|
||||
sha256sum eapim-portal.war eapim-portal-boot.war > SHA256SUMS
|
||||
'''
|
||||
archiveArtifacts artifacts: 'build/libs/eapim-portal.war,build/libs/eapim-portal-boot.war,build/libs/SHA1SUMS,build/libs/SHA256SUMS', fingerprint: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
|
||||
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
|
||||
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
|
||||
|
||||
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
|
||||
|
||||
|
||||
echo "========================"
|
||||
java -version
|
||||
javac -version
|
||||
echo "========================"
|
||||
gradle --version
|
||||
echo "========================"
|
||||
|
||||
gradle clean build -x test --no-daemon
|
||||
+5
-5
@@ -150,11 +150,11 @@ compileJava {
|
||||
}
|
||||
|
||||
processResources {
|
||||
exclude { details ->
|
||||
details.file.name.startsWith('application-') &&
|
||||
details.file.name.endsWith('.yml') &&
|
||||
!(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
}
|
||||
// exclude { details ->
|
||||
// details.file.name.startsWith('application-') &&
|
||||
// details.file.name.endsWith('.yml') &&
|
||||
// !(details.file.name in ['application-stage.yml', 'application-prod.yml'])
|
||||
// }
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
./gradlew bootWar
|
||||
docker build -t eactive-portal .
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the war file is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 /path/to/DEVPortal.war"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables
|
||||
WAR_FILE="$1"
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
DEST_DIR="/app/apim/deploy/DVPWeb"
|
||||
BACKUP_DIR="/app/apim/backup"
|
||||
|
||||
# Create backup directory if not exists
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup the existing directory
|
||||
if [ -d "$DEST_DIR" ]; then
|
||||
echo "Backing up existing directory..."
|
||||
mv $DEST_DIR "${BACKUP_DIR}/dvp_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
# Create destination directory if not exists
|
||||
mkdir -p $DEST_DIR
|
||||
|
||||
# Unzip the file to the dedicated location
|
||||
echo "Extracting WAR file to $DEST_DIR..."
|
||||
unzip $WAR_FILE -d $DEST_DIR
|
||||
|
||||
echo "deploy successful"
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the war file is provided
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 /path/to/DEVPortal.war"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables
|
||||
WAR_FILE="$1"
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
DEST_DIR="/app/apim/deploy/DVPWeb"
|
||||
STATIC_DIR="/app/apim/deploy/static"
|
||||
BACKUP_DIR="/app/apim/backup"
|
||||
|
||||
# Create backup directory if not exists
|
||||
mkdir -p $BACKUP_DIR
|
||||
|
||||
# Backup the existing directory
|
||||
if [ -d "$STATIC_DIR" ]; then
|
||||
echo "Backing up existing static directory..."
|
||||
mv $STATIC_DIR "${BACKUP_DIR}/dvp_static_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
if [ -d "$DEST_DIR" ]; then
|
||||
echo "Backing up existing directory..."
|
||||
mv $DEST_DIR "${BACKUP_DIR}/dvp_${TIMESTAMP}"
|
||||
fi
|
||||
|
||||
|
||||
# Create destination directory if not exists
|
||||
mkdir -p $DEST_DIR
|
||||
mkdir -p $STATIC_DIR
|
||||
|
||||
# unzip the file to the dedicated location
|
||||
echo "Extracting WAR file to $DEST_DIR..."
|
||||
unzip $WAR_FILE -d $DEST_DIR
|
||||
mv $DEST_DIR/static/* $STATIC_DIR/
|
||||
|
||||
echo "deploy successful"
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
export JAVA_HOME=/c/eactive/apps/jdk1.8.0_441
|
||||
export GRADLE_HOME=/c/eactive/apps/gradle-8.7
|
||||
export GRADLE_USER_HOME=/c/eactive/workspaces/gradle-user-home-kjbank
|
||||
|
||||
export PATH=$JAVA_HOME/bin:$GRADLE_HOME/bin:$PATH
|
||||
|
||||
|
||||
# gradle 명령을 대체합니다.
|
||||
command gradle $@
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.eactive.apim.portal.apps.community.notice.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IncidentAffectedApiDTO {
|
||||
private String apiId;
|
||||
private String apiName;
|
||||
}
|
||||
@@ -8,11 +8,18 @@ import org.hibernate.validator.constraints.Length;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PortalNoticeDTO {
|
||||
|
||||
public static final String NOTICE_TYPE_NORMAL = "1";
|
||||
public static final String NOTICE_TYPE_MAINTENANCE = "2";
|
||||
public static final String NOTICE_TYPE_INCIDENT = "3";
|
||||
|
||||
private String id;
|
||||
|
||||
@NotEmpty(message = "제목을 입력해주세요.")
|
||||
@@ -29,9 +36,33 @@ public class PortalNoticeDTO {
|
||||
|
||||
private boolean useYn;
|
||||
|
||||
private String fixYn;
|
||||
|
||||
private String noticeType;
|
||||
|
||||
private String inquirerName;
|
||||
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
// ───── 장애/점검 연동 필드 ─────
|
||||
private String summary;
|
||||
private LocalDateTime startedAt;
|
||||
private LocalDateTime endAt;
|
||||
private String state;
|
||||
private String previousState;
|
||||
private List<IncidentAffectedApiDTO> affectedApis = Collections.emptyList();
|
||||
|
||||
public boolean isIncidentType() {
|
||||
return NOTICE_TYPE_INCIDENT.equals(noticeType);
|
||||
}
|
||||
|
||||
public boolean isMaintenanceType() {
|
||||
return NOTICE_TYPE_MAINTENANCE.equals(noticeType);
|
||||
}
|
||||
|
||||
public boolean isIncidentOrMaintenance() {
|
||||
return isIncidentType() || isMaintenanceType();
|
||||
}
|
||||
}
|
||||
+45
-3
@@ -1,8 +1,12 @@
|
||||
package com.eactive.apim.portal.apps.community.notice.service;
|
||||
|
||||
import com.eactive.apim.portal.apps.community.notice.dto.IncidentAffectedApiDTO;
|
||||
import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeDTO;
|
||||
import com.eactive.apim.portal.apps.community.notice.dto.PortalNoticeSearch;
|
||||
import com.eactive.apim.portal.apps.community.notice.mapper.PortalNoticeMapper;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.entity.DjbApistatusIncident;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentApiRepository;
|
||||
import com.eactive.apim.portal.djb.apistatus.incident.repository.DjbApistatusIncidentRepository;
|
||||
import com.eactive.apim.portal.portalNotice.entity.PortalNotice;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -12,7 +16,10 @@ import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service("portalNoticeFacade")
|
||||
@RequiredArgsConstructor
|
||||
@@ -20,12 +27,13 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
|
||||
|
||||
private final PortalNoticeService portalNoticeService;
|
||||
private final PortalNoticeMapper portalNoticeMapper;
|
||||
private final DjbApistatusIncidentRepository incidentRepository;
|
||||
private final DjbApistatusIncidentApiRepository incidentApiRepository;
|
||||
|
||||
@Override
|
||||
public List<PortalNoticeDTO> getLatestNotices() {
|
||||
PortalNoticeSearch search = new PortalNoticeSearch();
|
||||
Pageable pageable = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "createdDate"));
|
||||
// Pageable pageable = Pageable.ofSize(3);
|
||||
Page<PortalNoticeDTO> notices = getNotices(search, pageable);
|
||||
return notices.getContent();
|
||||
}
|
||||
@@ -36,13 +44,47 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
|
||||
Specification<PortalNotice> spec = Specification.where(search.buildSpecification())
|
||||
.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("useYn"),"Y"));
|
||||
|
||||
Page<PortalNotice> noticesPage = portalNoticeService.getNotices(spec, pageable);
|
||||
// 상단고정(fixYn='Y') 우선 정렬 — 호출자의 sort 앞에 결합 (admin Service 와 일관)
|
||||
Sort fixYnFirst = Sort.by(Sort.Direction.DESC, "fixYn");
|
||||
Pageable fixedPageable = PageRequest.of(
|
||||
pageable.getPageNumber(),
|
||||
pageable.getPageSize(),
|
||||
fixYnFirst.and(pageable.getSort())
|
||||
);
|
||||
|
||||
Page<PortalNotice> noticesPage = portalNoticeService.getNotices(spec, fixedPageable);
|
||||
return noticesPage.map(portalNoticeMapper::map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PortalNoticeDTO getNotice(String id) {
|
||||
PortalNotice portalNotice = portalNoticeService.getNotice(id);
|
||||
return portalNoticeMapper.map(portalNotice);
|
||||
PortalNoticeDTO dto = portalNoticeMapper.map(portalNotice);
|
||||
populateIncident(dto);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private void populateIncident(PortalNoticeDTO dto) {
|
||||
if (!dto.isIncidentOrMaintenance()) {
|
||||
dto.setAffectedApis(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
Optional<DjbApistatusIncident> incidentOpt = incidentRepository.findByNoticeId(dto.getId());
|
||||
if (!incidentOpt.isPresent()) {
|
||||
dto.setAffectedApis(Collections.emptyList());
|
||||
return;
|
||||
}
|
||||
DjbApistatusIncident incident = incidentOpt.get();
|
||||
dto.setSummary(incident.getSummary());
|
||||
dto.setStartedAt(incident.getStartedAt());
|
||||
dto.setEndAt(incident.getEndAt());
|
||||
dto.setState(incident.getState() == null ? null : incident.getState().name());
|
||||
dto.setPreviousState(incident.getPreviousState() == null ? null : incident.getPreviousState().name());
|
||||
|
||||
List<IncidentAffectedApiDTO> apis = incidentApiRepository
|
||||
.findByIncidentIdOrderByApiId(incident.getIncidentId()).stream()
|
||||
.map(api -> new IncidentAffectedApiDTO(api.getApiId(), api.getApiName()))
|
||||
.collect(Collectors.toList());
|
||||
dto.setAffectedApis(apis);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ public class PartnershipApplicationController {
|
||||
partnershipApplicationFacade.createPartnershipApplication(partnershipApplicationDTO);
|
||||
|
||||
// 성공 메시지 추가
|
||||
redirectAttributes.addFlashAttribute("success", "사업제휴신청이 완료되었습니다.");
|
||||
redirectAttributes.addFlashAttribute("success", "피드백/개선요청 등록이 완료되었습니다.");
|
||||
|
||||
httpServletRequest.getSession().removeAttribute("previousPage");
|
||||
return "redirect:/partnership";
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ public class PartnershipApplicationFacadeImpl implements PartnershipApplicationF
|
||||
public void createPartnershipApplication(PartnershipApplicationDTO partnershipApplicationDTO) throws IOException {
|
||||
|
||||
FileInfo file = null;
|
||||
if (!partnershipApplicationDTO.getFiles().isEmpty()) {
|
||||
if (partnershipApplicationDTO.getFiles() != null && !partnershipApplicationDTO.getFiles().isEmpty()) {
|
||||
try {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
@@ -25,7 +25,7 @@ public class InquiryDTO {
|
||||
@Size(max = 4000, message = "제목은 50자를 초과할 수 없습니다.")
|
||||
private String inquiryDetail;
|
||||
|
||||
@Pattern(regexp = "^(PENDING|IN_PROGRESS|CLOSED)$", message = "유효하지 않은 문의 상태입니다.")
|
||||
@Pattern(regexp = "^(PENDING|RESPONDED|CLOSED)$", message = "유효하지 않은 문의 상태입니다.")
|
||||
private String inquiryStatus;
|
||||
|
||||
private String inquirerName;
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 문자열 마스킹 처리를 위한 유틸리티 클래스
|
||||
*
|
||||
* <p>보안아키텍처 표준 마스킹 규칙(elink-online-core {@code MaskingUtils})과 동일한 알고리즘을
|
||||
* 구현한다. eapim-portal은 elink-online-core에 의존하지 않으므로 로직을 직접 포함한다.
|
||||
* 소유자 본인이 자신의 정보를 조회하는 경우 마스킹하지 않는 owner 인지 동작은 유지한다.</p>
|
||||
*
|
||||
* <pre>
|
||||
* 이름 2자: 뒤 1자리(가나→가*) / 3자 이상: 앞뒤 제외 가운데 전체(가나다라→가**라)
|
||||
* 이메일 아이디 앞2·뒤2 마스킹, 4자 이하 전체(test→****, test123→**st1**)
|
||||
* 전화 2·3번째 세그먼트 각각 뒤 2자리(010-1234-1234→010-12**-12**)
|
||||
* </pre>
|
||||
*/
|
||||
public class StringMaskingUtil {
|
||||
|
||||
private static final char MASK = '*';
|
||||
|
||||
// ID 비교
|
||||
private static boolean isOwner(String ownerId, String currentUserId) {
|
||||
return currentUserId != null && ownerId != null && currentUserId.equals(ownerId);
|
||||
@@ -21,8 +35,15 @@ public class StringMaskingUtil {
|
||||
return name;
|
||||
}
|
||||
|
||||
return name.length() > 1 ?
|
||||
name.substring(0, name.length() - 1) + "*" : name;
|
||||
int len = name.length();
|
||||
if (len == 1) {
|
||||
return name;
|
||||
}
|
||||
if (len == 2) {
|
||||
return name.charAt(0) + String.valueOf(MASK);
|
||||
}
|
||||
// 3자 이상: 첫 자 + 가운데 전체 마스킹 + 마지막 자
|
||||
return name.charAt(0) + stars(len - 2) + name.charAt(len - 1);
|
||||
}
|
||||
|
||||
// 이메일 마스킹 처리
|
||||
@@ -31,32 +52,32 @@ public class StringMaskingUtil {
|
||||
return email;
|
||||
}
|
||||
|
||||
String[] parts = email.split("@", 2);
|
||||
if (parts.length != 2) {
|
||||
return email;
|
||||
int atIdx = email.indexOf('@');
|
||||
String local = email.substring(0, atIdx);
|
||||
String domain = email.substring(atIdx);
|
||||
|
||||
if (local.length() <= 4) {
|
||||
return stars(local.length()) + domain;
|
||||
}
|
||||
|
||||
String localPart = parts[0];
|
||||
String maskedLocal;
|
||||
|
||||
if (localPart.length() <= 2) {
|
||||
maskedLocal = "***";
|
||||
} else {
|
||||
maskedLocal = localPart.substring(0, localPart.length() - 3) + "***";
|
||||
}
|
||||
|
||||
return maskedLocal + "@" + parts[1];
|
||||
return stars(2) + local.substring(2, local.length() - 2) + stars(2) + domain;
|
||||
}
|
||||
|
||||
// 휴대폰 번호 마스킹 처리
|
||||
// 휴대폰/전화/FAX 번호 마스킹 처리
|
||||
public static String maskMobileNumber(String number, String ownerId, String currentUserId) {
|
||||
if (!isValidString(number) || isOwner(ownerId, currentUserId)) {
|
||||
return number;
|
||||
}
|
||||
|
||||
String[] parts = number.split("-", 3);
|
||||
return parts.length == 3 ?
|
||||
parts[0] + "-***-" + parts[2] : number;
|
||||
boolean hasHyphen = number.contains("-");
|
||||
String normalized = hasHyphen ? number : normalizePhoneNumber(number);
|
||||
String[] parts = normalized.split("-");
|
||||
if (parts.length == 3) {
|
||||
String m0 = parts[0];
|
||||
String m1 = maskSegmentTail(parts[1], 2);
|
||||
String m2 = maskSegmentTail(parts[2], 2);
|
||||
return hasHyphen ? m0 + "-" + m1 + "-" + m2 : m0 + m1 + m2;
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
// 기존 메서드 오버로딩 (하위 호환성)
|
||||
@@ -71,4 +92,41 @@ public class StringMaskingUtil {
|
||||
public static String maskMobileNumber(String number) {
|
||||
return maskMobileNumber(number, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// private helpers
|
||||
// =========================================================================
|
||||
|
||||
private static String stars(int count) {
|
||||
if (count <= 0) return "";
|
||||
char[] arr = new char[count];
|
||||
Arrays.fill(arr, MASK);
|
||||
return new String(arr);
|
||||
}
|
||||
|
||||
private static String maskSegmentTail(String segment, int count) {
|
||||
if (segment.length() <= count) return stars(segment.length());
|
||||
return segment.substring(0, segment.length() - count) + stars(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 한국 전화번호(숫자만) → XXX-XXXX-XXXX 형식으로 정규화
|
||||
* <pre>
|
||||
* 02 + 7자리 → 02-XXX-XXXX
|
||||
* 02 + 8자리 → 02-XXXX-XXXX
|
||||
* 0XX + 7자리 → 0XX-XXX-XXXX
|
||||
* 0XX + 8자리 → 0XX-XXXX-XXXX
|
||||
* </pre>
|
||||
*/
|
||||
private static String normalizePhoneNumber(String digits) {
|
||||
int len = digits.length();
|
||||
if (digits.startsWith("02")) {
|
||||
if (len == 9) return digits.substring(0, 2) + "-" + digits.substring(2, 5) + "-" + digits.substring(5);
|
||||
if (len == 10) return digits.substring(0, 2) + "-" + digits.substring(2, 6) + "-" + digits.substring(6);
|
||||
} else if (digits.startsWith("0")) {
|
||||
if (len == 10) return digits.substring(0, 3) + "-" + digits.substring(3, 6) + "-" + digits.substring(6);
|
||||
if (len == 11) return digits.substring(0, 3) + "-" + digits.substring(3, 7) + "-" + digits.substring(7);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
}
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.event;
|
||||
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.template.service.MessageEventHandler;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import com.eactive.apim.portal.template.service.MessageSendEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class InquiryCommentCreatedAdminEvent implements MessageEventHandler {
|
||||
|
||||
public static final MessageCode KEY = MessageCode.INQUIRY_COMMENT_CREATED;
|
||||
|
||||
@Override
|
||||
public MessageCode getKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowAdditionalRecipients() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Q&A 댓글 등록 알림";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "<div>사용 가능 변수: </div><br/>"
|
||||
+ "<div> - inquiryId: 문의 ID </div><br/>"
|
||||
+ "<div> - inquirySubject: 문의 제목 </div><br/>"
|
||||
+ "<div> - commentContent: 댓글 본문 </div><br/>"
|
||||
+ "<div> - writerName: 작성자 이름 </div><br/>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageSendEvent createEvent(Object source, MessageRecipient recipient, Map<String, Object> params) {
|
||||
Map<String, String> requestParams = new HashMap<>();
|
||||
requestParams.put("inquiryId", toStr(params.get("inquiryId")));
|
||||
requestParams.put("inquirySubject", toStr(params.get("inquirySubject")));
|
||||
requestParams.put("commentContent", toStr(params.get("commentContent")));
|
||||
requestParams.put("writerName", toStr(params.get("writerName")));
|
||||
return new MessageSendEvent(source, KEY, recipient, requestParams);
|
||||
}
|
||||
|
||||
private String toStr(Object value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ package com.eactive.apim.portal.djb.community.qna.constant;
|
||||
public final class DjbInquiryStatus {
|
||||
|
||||
public static final String PENDING = "PENDING";
|
||||
public static final String IN_PROGRESS = "IN_PROGRESS";
|
||||
public static final String RESPONDED = "RESPONDED";
|
||||
public static final String CLOSED = "CLOSED";
|
||||
|
||||
private DjbInquiryStatus() {
|
||||
|
||||
@@ -37,18 +37,6 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="NEED_FIX" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/need-fix.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<fileNamePattern>${LOG_PATH}/backup/need-fix.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
||||
<maxFileSize>500MB</maxFileSize>
|
||||
<maxHistory>90</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="HTTP_SESSION" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>${LOG_PATH}/http-session.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
@@ -66,10 +54,6 @@
|
||||
</appender>
|
||||
|
||||
|
||||
<logger name="eapim.portal.needfix" level="INFO" additivity="false">
|
||||
<appender-ref ref="NEED_FIX" />
|
||||
</logger>
|
||||
|
||||
<logger name="eapim.portal.session" level="INFO" additivity="false">
|
||||
<appender-ref ref="HTTP_SESSION" />
|
||||
</logger>
|
||||
@@ -96,51 +80,4 @@
|
||||
<springProfile name="stage,dev">
|
||||
|
||||
</springProfile>
|
||||
|
||||
|
||||
<springProfile name="gf63">
|
||||
<logger name="sun.rmi" level="INFO"/>
|
||||
|
||||
<logger name="javax" level="INFO"/>
|
||||
|
||||
<logger name="jdk.event.security" level="INFO"/>
|
||||
|
||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||
<logger name="com.atomikos" level="INFO"/>
|
||||
|
||||
<logger name="org.apache" level="INFO"/>
|
||||
<logger name="org.codehaus.groovy.vmplugin.VMPluginFactory" level="INFO"/>
|
||||
<!-- <logger name="org.hibernate" level="INFO" additivity="false"/>-->
|
||||
<logger name="org.thymeleaf.TemplateEngine" level="INFO"/>
|
||||
<!-- <logger name="org.springframework" level="INFO" />-->
|
||||
<logger name="_org.springframework.web" level="INFO" />
|
||||
|
||||
|
||||
<logger name="org.hibernate.SQL" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.type.descriptor.sql.BasicBinder" level="TRACE" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
<logger name="org.hibernate.engine.jdbc.spi.SqlExceptionHelper" additivity="false">
|
||||
<appender-ref ref="HIBERNATE_APPENDER" />
|
||||
</logger>
|
||||
|
||||
<logger name="com.eactive.eapim" level="DEBUG"/>
|
||||
<logger name="kjb.safedb" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="ROLLING"/>
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</logger>
|
||||
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="ROLLING"/>
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
</root>
|
||||
|
||||
<!-- <root level="DEBUG">-->
|
||||
<!-- <appender-ref ref="ROLLING"/>-->
|
||||
<!-- <appender-ref ref="CONSOLE"/>-->
|
||||
<!-- </root>-->
|
||||
</springProfile>
|
||||
</configuration>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -44,7 +44,15 @@
|
||||
function formatDate(value) {
|
||||
if (!value) return '';
|
||||
try {
|
||||
const d = new Date(value);
|
||||
var d;
|
||||
if (Array.isArray(value)) {
|
||||
// Jackson LocalDateTime 직렬화(timestamp 배열): [year, month(1-base), day, hour, minute, second, nano]
|
||||
var y = value[0], mo = value[1] || 1, da = value[2] || 1;
|
||||
var h = value[3] || 0, mi = value[4] || 0, s = value[5] || 0;
|
||||
d = new Date(y, mo - 1, da, h, mi, s);
|
||||
} else {
|
||||
d = new Date(value);
|
||||
}
|
||||
if (isNaN(d.getTime())) return '';
|
||||
const pad = function (n) { return n < 10 ? '0' + n : '' + n; };
|
||||
return d.getFullYear() + '.' + pad(d.getMonth() + 1) + '.' + pad(d.getDate())
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// Color Palette - Vibrant and Modern
|
||||
// Primary colors
|
||||
$primary-blue: #0049b4; // 광주은행 블루
|
||||
$primary-blue: #0049b4; // Vibrant blue
|
||||
$secondary-blue: #c3dfea; // Deep blue
|
||||
$accent-cyan: #00D4FF; // Sky blue accent
|
||||
$accent-yellow: #FFD93D; // Yellow accent
|
||||
@@ -35,7 +35,8 @@ $shadow-lg: 0 8px 24px rgba(75, 155, 255, 0.15);
|
||||
$shadow-xl: 0 16px 40px rgba(75, 155, 255, 0.2);
|
||||
|
||||
// Typography
|
||||
$font-family-primary: '원신한', 'OneShinhan', 'Spoqa Han Sans Neo', 'SpoqaHanSans', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-family-primary: 'Spoqa Han Sans Neo', 'SpoqaHanSans', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-family-heading: 'OneShinhan', 'Spoqa Han Sans Neo', 'SpoqaHanSans', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
$font-family-mono: 'Fira Code', 'Courier New', monospace;
|
||||
|
||||
// Font sizes
|
||||
|
||||
@@ -1,130 +1,97 @@
|
||||
@use '../abstracts/variables' as *;
|
||||
@use '../abstracts/color-functions' as *;
|
||||
@use '../abstracts/mixins' as *;
|
||||
// -----------------------------------------------------------------------------
|
||||
// Webfont @font-face declarations
|
||||
// - OneShinhan : 제목/강조 (Light 300, Medium 400·500, Bold 700)
|
||||
// - Spoqa Han Sans Neo / SpoqaHanSans : 본문 (Regular 400, Bold 700)
|
||||
// - Noto Sans KR : 한글 폴백 (Variable 100–900)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@font-face {
|
||||
font-family: 'Spoqa Han Sans Neo';
|
||||
font-weight: 700;
|
||||
src: local('Spoqa Han Sans Neo Bold'),
|
||||
url('/font/kjb/SpoqaHanSansNeo-Bold.woff2') format('woff2'),
|
||||
url('/font/kjb/SpoqaHanSansNeo-Bold.woff') format('woff'),
|
||||
url('/font/kjb/SpoqaHanSansNeo-Bold.ttf') format('truetype')
|
||||
;
|
||||
//unicode-range: U+AC00-D7AF, U+0000-0040, U+005B-0060, U+007B-007F
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Spoqa Han Sans Neo';
|
||||
font-weight: 400;
|
||||
src: local('Spoqa Han Sans Neo Regular'),
|
||||
url('/font/kjb/SpoqaHanSansNeo-Regular.woff2') format('woff2'),
|
||||
url('/font/kjb/SpoqaHanSansNeo-Regular.woff') format('woff'),
|
||||
url('/font/kjb/SpoqaHanSansNeo-Regular.ttf') format('truetype')
|
||||
;
|
||||
//unicode-range: U+AC00-D7AF, U+0000-0040, U+005B-0060, U+007B-007F
|
||||
}
|
||||
|
||||
/* OneShinhan — 웹폰트 서빙 (제주은행 inbank/css/font/ 원본을 정적 리소스로 호스팅)
|
||||
weight 300=Light / 400&500=Medium / 700=Bold */
|
||||
/* OneShinhan */
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
src: local('원신한 Light'),
|
||||
local('OneShinhan Light'),
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Light'),
|
||||
local('OneShinhan-Light'),
|
||||
url('/font/djb/OneShinhanLight.woff') format('woff');
|
||||
url('/font/shinhan/OneShinhanLight.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
src: local('원신한 Medium'),
|
||||
local('OneShinhan Medium'),
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Medium'),
|
||||
local('OneShinhan-Medium'),
|
||||
url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
url('/font/shinhan/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
src: local('원신한 Medium'),
|
||||
local('OneShinhan Medium'),
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Medium'),
|
||||
local('OneShinhan-Medium'),
|
||||
url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
url('/font/shinhan/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
src: local('원신한 Bold'),
|
||||
local('OneShinhan Bold'),
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Bold'),
|
||||
local('OneShinhan-Bold'),
|
||||
url('/font/djb/OneShinhanBold.woff') format('woff');
|
||||
}
|
||||
/* 한글 family name alias (변수에서 '원신한' 우선 매칭 시 동일 폰트 사용) */
|
||||
@font-face {
|
||||
font-family: '원신한';
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
src: url('/font/djb/OneShinhanLight.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: '원신한';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
src: url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: '원신한';
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
src: url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: '원신한';
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
src: url('/font/djb/OneShinhanBold.woff') format('woff');
|
||||
url('/font/shinhan/OneShinhanBold.woff') format('woff');
|
||||
}
|
||||
|
||||
|
||||
// KJB PIB
|
||||
/* SpoqaHanSans */
|
||||
/* Spoqa Han Sans Neo */
|
||||
@font-face {
|
||||
font-family:'SpoqaHanSans';
|
||||
font-family: 'Spoqa Han Sans Neo';
|
||||
font-weight: 400;
|
||||
src:url('/font/kjb/SpoqaHanSansRegular.woff') format('woff'),
|
||||
url('/font/kjb/SpoqaHanSansRegular.woff2') format('woff2'),
|
||||
url('/font/kjb/SpoqaHanSansRegular.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('Spoqa Han Sans Neo Regular'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansNeo-Regular.woff2') format('woff2'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansNeo-Regular.woff') format('woff'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansNeo-Regular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family:'SpoqaHanSansBold';
|
||||
font-family: 'Spoqa Han Sans Neo';
|
||||
font-weight: 700;
|
||||
src:url('/font/kjb/SpoqaHanSansBold.woff') format('woff'),
|
||||
url('/font/kjb/SpoqaHanSansBold.woff2') format('woff2'),
|
||||
url('/font/kjb/SpoqaHanSansBold.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('Spoqa Han Sans Neo Bold'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansNeo-Bold.woff2') format('woff2'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansNeo-Bold.woff') format('woff'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansNeo-Bold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* HGGGothicssi */
|
||||
/* SpoqaHanSans (Neo 미설치 환경 폴백) */
|
||||
@font-face {
|
||||
font-family:'HGGGothicssi';
|
||||
font-weight: 300;
|
||||
src:url('/font/kjb/HGGGothicssi40g.woff') format('woff'),
|
||||
url('/font/kjb/HGGGothicssi40g.woff2') format('woff2'),
|
||||
url('/font/kjb/HGGGothicssi40g.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family:'HGGGothicssi';
|
||||
font-family: 'SpoqaHanSans';
|
||||
font-weight: 400;
|
||||
src:url('/font/kjb/HGGGothicssi60g.woff') format('woff'),
|
||||
url('/font/kjb/HGGGothicssi60g.woff2') format('woff2'),
|
||||
url('/font/kjb/HGGGothicssi60g.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('Spoqa Han Sans Regular'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansRegular.woff2') format('woff2'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansRegular.woff') format('woff'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansRegular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family:'HGGGothicssi';
|
||||
font-family: 'SpoqaHanSans';
|
||||
font-weight: 700;
|
||||
src:url('/font/kjb/HGGGothicssi80g.woff') format('woff'),
|
||||
url('/font/kjb/HGGGothicssi80g.woff2') format('woff2'),
|
||||
url('/font/kjb/HGGGothicssi80g.ttf') format('truetype');
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('Spoqa Han Sans Bold'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansBold.woff2') format('woff2'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansBold.woff') format('woff'),
|
||||
url('/font/spoqa-hans/SpoqaHanSansBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* Noto Sans KR (Variable) */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans KR';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: url('/font/noto-sans/NotoSansKR-VariableFont_wght.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@@ -6,26 +6,38 @@
|
||||
// Basic typography styles
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Noto Sans KR Font Face
|
||||
@font-face {
|
||||
font-family: 'Noto Sans KR';
|
||||
src: url('/font/NotoSansKR-VariableFont_wght.ttf') format('truetype');
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
// Body text
|
||||
body {
|
||||
font-family: $font-family-primary;
|
||||
font-size: $font-size-base;
|
||||
font-weight: $font-weight-regular;
|
||||
color: $text-dark;
|
||||
}
|
||||
|
||||
// Headings
|
||||
// Headings — OneShinhan Medium
|
||||
h1, .h1,
|
||||
h2, .h2,
|
||||
h3, .h3,
|
||||
h4, .h4,
|
||||
h5, .h5,
|
||||
h6, .h6 {
|
||||
font-family: $font-family-heading;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
// "-title" / "__title" 으로 끝나는 클래스(.page-title, .section-title,
|
||||
// .row-cell--title, .oauth2-2legged__title …)도 사실상 제목 영역이므로
|
||||
// 동일하게 OneShinhan Medium 을 적용한다.
|
||||
[class$="-title"],
|
||||
[class*="-title "],
|
||||
[class$="__title"],
|
||||
[class*="__title "] {
|
||||
font-family: $font-family-heading;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
h1, .h1 {
|
||||
font-size: $font-size-5xl;
|
||||
font-weight: $font-weight-extrabold;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
@include respond-to('sm') {
|
||||
@@ -39,7 +51,6 @@ h1, .h1 {
|
||||
|
||||
h2, .h2 {
|
||||
font-size: $font-size-4xl;
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
@include respond-to('sm') {
|
||||
@@ -53,7 +64,6 @@ h2, .h2 {
|
||||
|
||||
h3, .h3 {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
@include respond-to('sm') {
|
||||
@@ -63,19 +73,16 @@ h3, .h3 {
|
||||
|
||||
h4, .h4 {
|
||||
font-size: $font-size-2xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
h5, .h5 {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
h6, .h6 {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: $font-weight-semibold;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,7 @@ $apikey-border-color: #dadada;
|
||||
}
|
||||
|
||||
.common-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 22px;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #E65100;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__description {
|
||||
|
||||
@@ -569,9 +569,7 @@
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #212529;
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
@@ -36,9 +36,7 @@
|
||||
}
|
||||
|
||||
.app-list-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 28px;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
|
||||
|
||||
@@ -150,9 +150,7 @@
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 46px;
|
||||
font-weight: 700;
|
||||
line-height: 99.94%;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
@@ -500,9 +498,7 @@
|
||||
}
|
||||
|
||||
.search-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
@@ -820,9 +816,7 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
@@ -988,9 +982,7 @@
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.67;
|
||||
color: #2A2A2A;
|
||||
margin: 0 0 20px 0;
|
||||
@@ -1137,9 +1129,7 @@
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: #000000;
|
||||
margin: 0 0 $spacing-lg 0;
|
||||
@@ -1402,9 +1392,7 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 28px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
color: #000000;
|
||||
margin: 0 0 $spacing-lg 0;
|
||||
@@ -1793,7 +1781,6 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: $font-family-primary;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
|
||||
@@ -2061,9 +2048,7 @@
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
|
||||
@@ -31,7 +31,6 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
font-family: '원신한', 'OneShinhan', 'Spoqa Han Sans Neo', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
@@ -39,7 +38,6 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 25px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
@@ -66,7 +64,6 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
|
||||
&__section-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -130,7 +127,6 @@ $service-card-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgb
|
||||
|
||||
&__title {
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: $service-primary-blue;
|
||||
margin: 0 0 22px 0;
|
||||
@@ -689,7 +685,6 @@ $o2leg-err-fg: #a23b3b;
|
||||
padding: 20px;
|
||||
background-color: #FFFFFF;
|
||||
color: $o2leg-text-dark;
|
||||
font-family: '원신한', 'OneShinhan', 'Spoqa Han Sans Neo', sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
|
||||
@@ -384,6 +384,11 @@
|
||||
&--pending {
|
||||
background-color: #8c959f;
|
||||
}
|
||||
|
||||
// 종료 - 짙은 회색 배경
|
||||
&--closed {
|
||||
background-color: #4a5560;
|
||||
}
|
||||
}
|
||||
|
||||
// Inquiry Actions
|
||||
@@ -694,25 +699,25 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 제목 컬럼 - flex 1
|
||||
// 제목 컬럼 - flex 1 (인라인 min-width:200px 무력화로 모바일 축소 허용)
|
||||
&:nth-child(2) {
|
||||
flex: 1 !important;
|
||||
min-width: 100px;
|
||||
flex: 1 1 0 !important;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
|
||||
// 작성자 컬럼 - 50px, 가운데 정렬
|
||||
// 작성자 컬럼 - 60px, 가운데 정렬
|
||||
&:nth-child(3) {
|
||||
width: 50px !important;
|
||||
min-width: 50px;
|
||||
flex: none;
|
||||
width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
flex: none !important;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 처리상태 컬럼 - 65px, 가운데 정렬
|
||||
// 처리상태 컬럼 - 78px, 가운데 정렬
|
||||
&:nth-child(4) {
|
||||
width: 65px !important;
|
||||
min-width: 65px;
|
||||
flex: none;
|
||||
width: 78px !important;
|
||||
min-width: 78px !important;
|
||||
flex: none !important;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -731,11 +736,12 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 제목 컬럼 - flex 1
|
||||
// 제목 컬럼 - flex 1 (인라인 min-width:200px 무력화로 모바일 축소 허용)
|
||||
&:nth-child(2),
|
||||
&.row-cell--title {
|
||||
flex: 1 !important;
|
||||
min-width: 100px;
|
||||
flex: 1 1 0 !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden;
|
||||
justify-content: flex-start;
|
||||
|
||||
.notice-title-link {
|
||||
@@ -768,20 +774,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 작성자 컬럼 - 50px, 가운데 정렬
|
||||
// 작성자 컬럼 - 60px, 가운데 정렬 (마스킹 이름 넘침 방지)
|
||||
&:nth-child(3) {
|
||||
width: 50px !important;
|
||||
min-width: 50px;
|
||||
flex: none;
|
||||
width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
flex: none !important;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 처리상태 컬럼 - 65px, 가운데 정렬
|
||||
// 처리상태 컬럼 - 78px, 가운데 정렬
|
||||
&:nth-child(4) {
|
||||
width: 65px !important;
|
||||
min-width: 65px;
|
||||
flex: none;
|
||||
width: 78px !important;
|
||||
min-width: 78px !important;
|
||||
flex: none !important;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<div class="inquiry-detail-header">
|
||||
<div class="inquiry-header-top">
|
||||
<span class="inquiry-status-badge"
|
||||
th:classappend="${inquiry.inquiryStatus == 'IN_PROGRESS' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'IN_PROGRESS' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
답변대기
|
||||
</span>
|
||||
<div class="inquiry-header-right">
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Admin Response Section -->
|
||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'IN_PROGRESS' or inquiry.inquiryStatus == 'CLOSED'}">
|
||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'RESPONDED' or inquiry.inquiryStatus == 'CLOSED'}">
|
||||
<div class="inquiry-response-header">
|
||||
<span class="response-label">관리자 답변</span>
|
||||
<span class="response-date" th:text="${#temporals.format(inquiry.responseDate, 'yyyy.MM.dd')}">2025.11.02</span>
|
||||
|
||||
@@ -70,8 +70,8 @@
|
||||
<div class="row-cell" style="width: 100px;" data-label="작성자" th:text="${inquiry.maskedInquirerName}">홍**</div>
|
||||
<div class="row-cell" style="width: 120px;" data-label="처리상태">
|
||||
<span class="inquiry-status-badge"
|
||||
th:classappend="${inquiry.inquiryStatus == 'IN_PROGRESS' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'IN_PROGRESS' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
답변대기
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,50 @@
|
||||
|
||||
<!-- Notice Header: Title and Date -->
|
||||
<div class="notice-detail-header">
|
||||
<h2 class="notice-detail-title" th:text="${portalNotice.noticeSubject}">개인정보처리 방침 정정 공지</h2>
|
||||
<h2 class="notice-detail-title">
|
||||
<span th:if="${portalNotice.noticeType == '3'}"
|
||||
style="display:inline-block;padding:2px 10px;margin-right:8px;border-radius:12px;font-size:14px;vertical-align:middle;background:#fde7e9;color:#c0152a;">장애</span>
|
||||
<span th:if="${portalNotice.noticeType == '2'}"
|
||||
style="display:inline-block;padding:2px 10px;margin-right:8px;border-radius:12px;font-size:14px;vertical-align:middle;background:#fff4d6;color:#9a6700;">점검</span>
|
||||
<span th:text="${portalNotice.noticeSubject}">개인정보처리 방침 정정 공지</span>
|
||||
</h2>
|
||||
<span class="notice-detail-date" th:text="${#temporals.format(portalNotice.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
|
||||
</div>
|
||||
|
||||
<!-- 장애/점검 정보 영역 -->
|
||||
<div class="notice-detail-incident-info" th:if="${portalNotice.incidentOrMaintenance}"
|
||||
style="margin:16px 0;padding:16px;background:#f7f8fa;border:1px solid #e3e6eb;border-radius:8px;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<colgroup>
|
||||
<col style="width:120px;"><col><col style="width:120px;"><col>
|
||||
</colgroup>
|
||||
<tr>
|
||||
<th style="text-align:left;padding:6px 8px;">시작</th>
|
||||
<td style="padding:6px 8px;"
|
||||
th:text="${portalNotice.startedAt != null ? #temporals.format(portalNotice.startedAt, 'yyyy.MM.dd HH:mm') : '-'}">-</td>
|
||||
<th style="text-align:left;padding:6px 8px;">종료</th>
|
||||
<td style="padding:6px 8px;"
|
||||
th:text="${portalNotice.endAt != null ? #temporals.format(portalNotice.endAt, 'yyyy.MM.dd HH:mm') : '진행중'}">-</td>
|
||||
</tr>
|
||||
<tr th:if="${portalNotice.incidentType}">
|
||||
<th style="text-align:left;padding:6px 8px;">상태</th>
|
||||
<td colspan="3" style="padding:6px 8px;" th:text="${portalNotice.state != null ? portalNotice.state : '-'}">-</td>
|
||||
</tr>
|
||||
<tr th:if="${portalNotice.affectedApis != null and !portalNotice.affectedApis.isEmpty()}">
|
||||
<th style="text-align:left;padding:6px 8px;vertical-align:top;">영향 API</th>
|
||||
<td colspan="3" style="padding:6px 8px;">
|
||||
<ul style="margin:0;padding-left:18px;">
|
||||
<li th:each="api : ${portalNotice.affectedApis}">
|
||||
<strong th:text="${api.apiId}">API_ID</strong>
|
||||
<span th:if="${api.apiName != null and !api.apiName.isEmpty()}"
|
||||
th:text="| - ${api.apiName}|"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Attachment Section -->
|
||||
<div class="notice-detail-attachment" th:if="${!#strings.isEmpty(portalNotice.fileId)}">
|
||||
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(portalNotice.fileId)}">
|
||||
|
||||
@@ -58,6 +58,15 @@
|
||||
<div class="row-cell row-cell--title" style="flex: 1; min-width: 200px;" data-label="제목">
|
||||
<a th:href="@{/portalnotice/detail(id=${notice.id})}" class="notice-title-link">
|
||||
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
|
||||
<span class="notice-fix-badge"
|
||||
th:if="${notice.fixYn == 'Y'}"
|
||||
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#e5f0ff;color:#0a4ea3;font-weight:600;">고정</span>
|
||||
<span class="notice-type-badge notice-type-badge--incident"
|
||||
th:if="${notice.noticeType == '3'}"
|
||||
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#fde7e9;color:#c0152a;">장애</span>
|
||||
<span class="notice-type-badge notice-type-badge--maintenance"
|
||||
th:if="${notice.noticeType == '2'}"
|
||||
style="display:inline-block;padding:2px 8px;margin-right:6px;border-radius:10px;font-size:12px;background:#fff4d6;color:#9a6700;">점검</span>
|
||||
<span th:text="${notice.noticeSubject}">공지사항 제목</span>
|
||||
<span class="file-icon" th:if="${notice.fileId != null and !notice.fileId.isEmpty()}">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -189,7 +189,7 @@
|
||||
처음 만나는 DJBank API
|
||||
</a>
|
||||
<a href="#" class="action-btn action-btn-secondary">
|
||||
사업 제휴 문의
|
||||
피드백/개선요청
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<li><a th:href="@{/portalnotice}">공지사항</a></li>
|
||||
<li><a th:href="@{/faq_list}">FAQ</a></li>
|
||||
<li><a th:href="@{/inquiry}">Q&A</a></li>
|
||||
<li><a th:href="@{/partnership}">사업 제휴 문의</a></li>
|
||||
<li><a th:href="@{/partnership}">피드백/개선요청</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -81,7 +81,6 @@
|
||||
<li sec:authorize="hasRole('ROLE_APP')">
|
||||
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>인증 키 관리</a>
|
||||
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
|
||||
<a th:href="@{/commission/manage}"><i class="fas fa-money-bill"></i>과금 관리</a>
|
||||
</li>
|
||||
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
|
||||
<li><a th:href="@{/change_password}"><i class="fas fa-lock"></i>비밀번호 변경</a></li>
|
||||
@@ -176,6 +175,7 @@
|
||||
<ul class="drawer-submenu">
|
||||
<li><a th:href="@{/service/intro}">API 포탈 소개</a></li>
|
||||
<li><a th:href="@{/service/guide}">회원가입 안내</a></li>
|
||||
<li><a th:href="@{/service/oauth2-guide}">OAuth2 개발가이드</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.eactive.apim.portal.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* eapim-portal {@link StringMaskingUtil}가 보안아키텍처 표준 규칙으로 마스킹하며,
|
||||
* 소유자 본인 조회 시에는 마스킹하지 않는 동작을 검증한다.
|
||||
*/
|
||||
class StringMaskingUtilTest {
|
||||
|
||||
// --- 이름 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("이름 — 2자: 뒤 1자리")
|
||||
void maskName_2chars() {
|
||||
assertEquals("가*", StringMaskingUtil.maskName("가나"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이름 — 3자: 가운데 1자리")
|
||||
void maskName_3chars() {
|
||||
assertEquals("가*다", StringMaskingUtil.maskName("가나다"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이름 — 4자: 앞뒤 제외 가운데 전체")
|
||||
void maskName_4chars() {
|
||||
assertEquals("가**라", StringMaskingUtil.maskName("가나다라"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이름 — 5자: 앞뒤 제외 가운데 전체")
|
||||
void maskName_5chars() {
|
||||
assertEquals("가***마", StringMaskingUtil.maskName("가나다라마"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이름 — 1자/null/빈문자 방어")
|
||||
void maskName_oneAndNull() {
|
||||
assertEquals("가", StringMaskingUtil.maskName("가"));
|
||||
assertNull(StringMaskingUtil.maskName(null));
|
||||
assertEquals("", StringMaskingUtil.maskName(""));
|
||||
}
|
||||
|
||||
// --- 이메일 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("이메일 — 아이디 앞2·뒤2 마스킹")
|
||||
void maskEmail_normal() {
|
||||
assertEquals("**st1**@test.com", StringMaskingUtil.maskEmail("test123@test.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이메일 — 아이디 4자: 전체 마스킹")
|
||||
void maskEmail_fourChars() {
|
||||
assertEquals("****@test.com", StringMaskingUtil.maskEmail("test@test.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이메일 — 아이디 1자: 전체 마스킹")
|
||||
void maskEmail_oneChar() {
|
||||
assertEquals("*@123.com", StringMaskingUtil.maskEmail("1@123.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이메일 — @ 없으면 원본 / null·빈문자 방어")
|
||||
void maskEmail_invalidAndNull() {
|
||||
assertEquals("notanemail", StringMaskingUtil.maskEmail("notanemail"));
|
||||
assertNull(StringMaskingUtil.maskEmail(null));
|
||||
assertEquals("", StringMaskingUtil.maskEmail(""));
|
||||
}
|
||||
|
||||
// --- 휴대폰/전화 ---
|
||||
|
||||
@Test
|
||||
@DisplayName("휴대폰 — 2·3번째 세그먼트 각 뒤 2자리")
|
||||
void maskMobileNumber_normal() {
|
||||
assertEquals("010-12**-12**", StringMaskingUtil.maskMobileNumber("010-1234-1234"));
|
||||
assertEquals("010-12**-56**", StringMaskingUtil.maskMobileNumber("010-1234-5678"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("휴대폰 — 하이픈 없는 11자리")
|
||||
void maskMobileNumber_noHyphen() {
|
||||
assertEquals("01012**56**", StringMaskingUtil.maskMobileNumber("01012345678"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("휴대폰 — null/빈문자 방어")
|
||||
void maskMobileNumber_null() {
|
||||
assertNull(StringMaskingUtil.maskMobileNumber(null));
|
||||
assertEquals("", StringMaskingUtil.maskMobileNumber(""));
|
||||
}
|
||||
|
||||
// --- owner 인지 (본인 조회 시 비마스킹) ---
|
||||
|
||||
@Test
|
||||
@DisplayName("owner — 본인 조회 시 마스킹하지 않음")
|
||||
void ownerAware_noMaskWhenOwner() {
|
||||
assertEquals("가나다", StringMaskingUtil.maskName("가나다", "user1", "user1"));
|
||||
assertEquals("test123@test.com", StringMaskingUtil.maskEmail("test123@test.com", "user1", "user1"));
|
||||
assertEquals("010-1234-1234", StringMaskingUtil.maskMobileNumber("010-1234-1234", "user1", "user1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("owner — 타인 조회 시 마스킹")
|
||||
void ownerAware_maskWhenNotOwner() {
|
||||
assertEquals("가*다", StringMaskingUtil.maskName("가나다", "user1", "user2"));
|
||||
assertEquals("**st1**@test.com", StringMaskingUtil.maskEmail("test123@test.com", "user1", "user2"));
|
||||
assertEquals("010-12**-12**", StringMaskingUtil.maskMobileNumber("010-1234-1234", "user1", "user2"));
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -54,7 +54,7 @@ class InquiryCommentPermissionCheckerTest {
|
||||
|
||||
@Test
|
||||
void canWrite_inProgressTrue() {
|
||||
assertTrue(checker.canWrite(inquiry("IN_PROGRESS")));
|
||||
assertTrue(checker.canWrite(inquiry("RESPONDED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -64,14 +64,14 @@ class InquiryCommentPermissionCheckerTest {
|
||||
|
||||
@Test
|
||||
void canDelete_ownerInOpenTrue() {
|
||||
Inquiry i = inquiry("IN_PROGRESS");
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertTrue(checker.canDelete(c, user("alice")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canDelete_otherUserFalse() {
|
||||
Inquiry i = inquiry("IN_PROGRESS");
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertFalse(checker.canDelete(c, user("bob")));
|
||||
}
|
||||
@@ -102,7 +102,7 @@ class InquiryCommentPermissionCheckerTest {
|
||||
|
||||
@Test
|
||||
void assertDeletable_notOwnerThrowsNotOwned() {
|
||||
Inquiry i = inquiry("IN_PROGRESS");
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertThrows(InquiryCommentNotOwnedException.class,
|
||||
() -> checker.assertDeletable(c, user("bob")));
|
||||
@@ -110,7 +110,7 @@ class InquiryCommentPermissionCheckerTest {
|
||||
|
||||
@Test
|
||||
void assertDeletable_ownerInOpenOk() {
|
||||
Inquiry i = inquiry("IN_PROGRESS");
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertDoesNotThrow(() -> checker.assertDeletable(c, user("alice")));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user