Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67ec6d2103 | |||
| b024f8e86f | |||
| fd16300553 | |||
| 815c329e3c | |||
| 7fdd70dbc7 | |||
| 3dd9edc849 | |||
| 6cff96f40a | |||
| a97609d533 |
+10
-45
@@ -1,6 +1,10 @@
|
||||
pipeline {
|
||||
agent { label 'djb-vm' }
|
||||
|
||||
parameters {
|
||||
string(name: 'BRANCH', defaultValue: 'master', description: 'Branch to deploy')
|
||||
}
|
||||
|
||||
options {
|
||||
timestamps()
|
||||
disableConcurrentBuilds()
|
||||
@@ -19,13 +23,16 @@ pipeline {
|
||||
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
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: "*/${params.BRANCH}"]],
|
||||
userRemoteConfigs: [[url: 'ssh://git@172.30.1.50:2222/djb-eapim/eapim-portal.git']]
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +43,6 @@ pipeline {
|
||||
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
|
||||
@@ -48,7 +54,6 @@ pipeline {
|
||||
|
||||
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
|
||||
@@ -71,21 +76,9 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
sh 'gradle clean build -x test --no-daemon'
|
||||
}
|
||||
post {
|
||||
success {
|
||||
@@ -186,32 +179,4 @@ pipeline {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,17 @@
|
||||
#!/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 {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
./gradlew bootWar
|
||||
docker build -t eactive-portal .
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/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
@@ -1,13 +0,0 @@
|
||||
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,18 +8,11 @@ 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 = "제목을 입력해주세요.")
|
||||
@@ -36,33 +29,9 @@ 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();
|
||||
}
|
||||
}
|
||||
+3
-45
@@ -1,12 +1,8 @@
|
||||
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;
|
||||
@@ -16,10 +12,7 @@ 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
|
||||
@@ -27,13 +20,12 @@ 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();
|
||||
}
|
||||
@@ -44,47 +36,13 @@ public class PortalNoticeFacadeImpl implements PortalNoticeFacade {
|
||||
Specification<PortalNotice> spec = Specification.where(search.buildSpecification())
|
||||
.and((root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("useYn"),"Y"));
|
||||
|
||||
// 상단고정(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);
|
||||
Page<PortalNotice> noticesPage = portalNoticeService.getNotices(spec, pageable);
|
||||
return noticesPage.map(portalNoticeMapper::map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PortalNoticeDTO getNotice(String id) {
|
||||
PortalNotice portalNotice = portalNoticeService.getNotice(id);
|
||||
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);
|
||||
return portalNoticeMapper.map(portalNotice);
|
||||
}
|
||||
}
|
||||
|
||||
+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() != null && !partnershipApplicationDTO.getFiles().isEmpty()) {
|
||||
if (!partnershipApplicationDTO.getFiles().isEmpty()) {
|
||||
try {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
+2
-15
@@ -6,11 +6,7 @@ import com.eactive.apim.portal.apps.community.qna.service.InquiryFacade;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.service.InquiryCommentFacade;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -33,12 +29,9 @@ public class InquiryController {
|
||||
public static final String APPS_COMMUNITY_MAIN_INQUIRY_FORM = "apps/community/mainInquiryForm";
|
||||
public static final String REDIRECT_INQUIRY = "redirect:/inquiry";
|
||||
private final InquiryFacade inquiryFacade;
|
||||
private final InquiryCommentFacade inquiryCommentFacade;
|
||||
|
||||
public InquiryController(InquiryFacade inquiryFacade,
|
||||
InquiryCommentFacade inquiryCommentFacade) {
|
||||
public InquiryController(InquiryFacade inquiryFacade) {
|
||||
this.inquiryFacade = inquiryFacade;
|
||||
this.inquiryCommentFacade = inquiryCommentFacade;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,13 +42,8 @@ public class InquiryController {
|
||||
Model model) {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
Page<InquiryDTO> inquiries = inquiryFacade.getInquiries(search, pageable, user);
|
||||
List<String> ids = inquiries.getContent().stream()
|
||||
.map(InquiryDTO::getId)
|
||||
.collect(Collectors.toList());
|
||||
Map<String, Long> commentCounts = inquiryCommentFacade.countActiveByInquiryIds(ids);
|
||||
model.addAttribute("inquiries", inquiries.getContent());
|
||||
model.addAttribute("page", inquiries);
|
||||
model.addAttribute("commentCounts", commentCounts);
|
||||
return "apps/community/mainInquiryList";
|
||||
}
|
||||
|
||||
@@ -69,9 +57,8 @@ public class InquiryController {
|
||||
return REDIRECT_INQUIRY;
|
||||
}
|
||||
|
||||
InquiryDTO inquiry = inquiryFacade.getAccessibleInquiry(SecurityUtil.getPortalAuthenticatedUser(), id);
|
||||
InquiryDTO inquiry = inquiryFacade.getMyInquiry(SecurityUtil.getPortalAuthenticatedUser(), id);
|
||||
model.addAttribute("inquiry", inquiry);
|
||||
model.addAttribute("commentWritable", !"CLOSED".equals(inquiry.getInquiryStatus()));
|
||||
return "apps/community/mainInquiryDetail";
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@@ -25,7 +28,7 @@ public class InquiryDTO {
|
||||
@Size(max = 4000, message = "제목은 50자를 초과할 수 없습니다.")
|
||||
private String inquiryDetail;
|
||||
|
||||
@Pattern(regexp = "^(PENDING|RESPONDED|CLOSED)$", message = "유효하지 않은 문의 상태입니다.")
|
||||
@Pattern(regexp = "^(PENDING|IN_PROGRESS|CLOSED)$", message = "유효하지 않은 문의 상태입니다.")
|
||||
private String inquiryStatus;
|
||||
|
||||
private String inquirerName;
|
||||
@@ -47,6 +50,8 @@ public class InquiryDTO {
|
||||
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
private List<MultipartFile> files;
|
||||
|
||||
private String attachFile;
|
||||
|
||||
private String maskedInquirerName;
|
||||
|
||||
@@ -15,8 +15,6 @@ public interface InquiryFacade {
|
||||
|
||||
InquiryDTO getMyInquiry(PortalAuthenticatedUser user, String id);
|
||||
|
||||
InquiryDTO getAccessibleInquiry(PortalAuthenticatedUser user, String id);
|
||||
|
||||
void createInquiry(InquiryDTO inquiryDTO) throws IOException;
|
||||
|
||||
void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException;
|
||||
|
||||
+42
-16
@@ -6,10 +6,12 @@ import com.eactive.apim.portal.apps.community.qna.mapper.InquiryMapper;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.common.util.StringMaskingUtil;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.service.InquiryAdminNotifier;
|
||||
import com.eactive.apim.portal.common.util.UserTypeUtil;
|
||||
import com.eactive.apim.portal.file.entity.FileInfo;
|
||||
import com.eactive.apim.portal.file.service.FileService;
|
||||
import com.eactive.apim.portal.file.service.FileTypeContext;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -18,15 +20,14 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InquiryFacadeImpl implements InquiryFacade {
|
||||
|
||||
private final FileService fileService;
|
||||
private final InquiryService inquiryService;
|
||||
private final InquiryMapper inquiryMapper;
|
||||
private final InquiryAdminNotifier inquiryAdminNotifier;
|
||||
|
||||
@Override
|
||||
public Page<InquiryDTO> getInquiries(InquirySearch search, Pageable pageable, PortalAuthenticatedUser user) {
|
||||
@@ -79,29 +80,54 @@ public class InquiryFacadeImpl implements InquiryFacade {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InquiryDTO getAccessibleInquiry(PortalAuthenticatedUser user, String id) {
|
||||
Inquiry inquiry = inquiryService.getAccessibleInquiry(user, id);
|
||||
return inquiryMapper.map(inquiry);
|
||||
public void createInquiry(InquiryDTO inquiryDTO) throws IOException {
|
||||
|
||||
FileInfo file = null;
|
||||
if (!inquiryDTO.getFiles().isEmpty()) {
|
||||
try {
|
||||
PortalAuthenticatedUser user = SecurityUtil.getPortalAuthenticatedUser();
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
FileTypeContext.setFileType("qna");
|
||||
file = fileService.createFile(inquiryDTO.getFiles());
|
||||
} finally {
|
||||
FileService.clearInternalUserContext();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createInquiry(InquiryDTO inquiryDTO) throws IOException {
|
||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
||||
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
||||
inquiry.setInquirer(current);
|
||||
if (file != null) {
|
||||
inquiry.setAttachFile(file.getFileId());
|
||||
}
|
||||
inquiry.setInquirer(SecurityUtil.getPortalAuthenticatedUser());
|
||||
inquiryService.createInquiry(inquiry);
|
||||
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
HashMap<String, Object> params = new HashMap<>();
|
||||
params.put("inquiryId", inquiry.getId());
|
||||
params.put("inquirySubject", inquiry.getInquirySubject());
|
||||
params.put("writerName", current.getUserName());
|
||||
inquiryAdminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_CREATED, params);
|
||||
params.put("subject", inquiry.getInquirySubject());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateUserInquiry(PortalAuthenticatedUser user, String id, InquiryDTO inquiryDTO) throws IOException {
|
||||
Inquiry existingInquiry = inquiryService.getMyInquiry(user, id);
|
||||
inquiryDTO.setAttachFile(existingInquiry.getAttachFile());
|
||||
|
||||
if (!inquiryDTO.getFiles().isEmpty() && !inquiryDTO.getFiles().get(0).isEmpty()) {
|
||||
try {
|
||||
FileService.setInternalUserContext(UserTypeUtil.isInternalUser(user));
|
||||
|
||||
FileTypeContext.setFileType("qna");
|
||||
FileInfo file = fileService.createFile(inquiryDTO.getFiles());
|
||||
if(file != null) {
|
||||
inquiryDTO.setAttachFile(file.getFileId());
|
||||
}
|
||||
} finally {
|
||||
FileService.clearInternalUserContext();
|
||||
}
|
||||
} else if (existingInquiry.getAttachFile() != null && inquiryDTO.getAttachFile() == null) {
|
||||
fileService.deleteFile(inquiryDTO.getAttachFile());
|
||||
inquiryDTO.setAttachFile(null);
|
||||
}
|
||||
|
||||
Inquiry inquiry = inquiryMapper.toEntity(inquiryDTO);
|
||||
inquiry.setInquirer(existingInquiry.getInquirer());
|
||||
|
||||
@@ -4,9 +4,6 @@ package com.eactive.apim.portal.apps.community.qna.service;
|
||||
import com.eactive.apim.portal.apps.community.qna.repository.InquiryRepository;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.portalorg.entity.PortalOrg;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -93,36 +90,5 @@ public class InquiryService {
|
||||
return inquiryRepository.findByInquirerAndId(user, id).orElseThrow(() -> new NotFoundException("내 질문을 찾을 수 없습니다."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 본인 또는 같은 법인 소속 이용자가 작성한 Q&A를 조회한다.
|
||||
* - ROLE_USER: 본인 글만 접근 가능
|
||||
* - ROLE_CORP_*: 같은 법인 소속 이용자가 작성한 글까지 접근 가능
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Inquiry getAccessibleInquiry(PortalAuthenticatedUser user, String id) {
|
||||
Inquiry inquiry = getInquiry(id);
|
||||
if (!isAccessibleByUser(user, inquiry)) {
|
||||
throw new NotFoundException("문의글을 찾을 수 없습니다. id=" + id);
|
||||
}
|
||||
return inquiry;
|
||||
}
|
||||
|
||||
private boolean isAccessibleByUser(PortalAuthenticatedUser user, Inquiry inquiry) {
|
||||
PortalUser inquirer = inquiry.getInquirer();
|
||||
if (inquirer == null || user == null) {
|
||||
return false;
|
||||
}
|
||||
if (user.getId() != null && user.getId().equals(inquirer.getId())) {
|
||||
return true;
|
||||
}
|
||||
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_USER) {
|
||||
return false;
|
||||
}
|
||||
PortalOrg userOrg = user.getPortalOrg();
|
||||
PortalOrg inquirerOrg = inquirer.getPortalOrg();
|
||||
return userOrg != null && inquirerOrg != null
|
||||
&& userOrg.getId() != null
|
||||
&& userOrg.getId().equals(inquirerOrg.getId());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,24 +1,10 @@
|
||||
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);
|
||||
@@ -35,15 +21,8 @@ public class StringMaskingUtil {
|
||||
return 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);
|
||||
return name.length() > 1 ?
|
||||
name.substring(0, name.length() - 1) + "*" : name;
|
||||
}
|
||||
|
||||
// 이메일 마스킹 처리
|
||||
@@ -52,32 +31,32 @@ public class StringMaskingUtil {
|
||||
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;
|
||||
}
|
||||
return stars(2) + local.substring(2, local.length() - 2) + stars(2) + domain;
|
||||
String[] parts = email.split("@", 2);
|
||||
if (parts.length != 2) {
|
||||
return email;
|
||||
}
|
||||
|
||||
// 휴대폰/전화/FAX 번호 마스킹 처리
|
||||
String localPart = parts[0];
|
||||
String maskedLocal;
|
||||
|
||||
if (localPart.length() <= 2) {
|
||||
maskedLocal = "***";
|
||||
} else {
|
||||
maskedLocal = localPart.substring(0, localPart.length() - 3) + "***";
|
||||
}
|
||||
|
||||
return maskedLocal + "@" + parts[1];
|
||||
}
|
||||
|
||||
// 휴대폰 번호 마스킹 처리
|
||||
public static String maskMobileNumber(String number, String ownerId, String currentUserId) {
|
||||
if (!isValidString(number) || isOwner(ownerId, currentUserId)) {
|
||||
return 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;
|
||||
String[] parts = number.split("-", 3);
|
||||
return parts.length == 3 ?
|
||||
parts[0] + "-***-" + parts[2] : number;
|
||||
}
|
||||
|
||||
// 기존 메서드 오버로딩 (하위 호환성)
|
||||
@@ -92,41 +71,4 @@ 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;
|
||||
}
|
||||
}
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.controller;
|
||||
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.common.util.SecurityUtil;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentCreateRequest;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.service.InquiryCommentFacade;
|
||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryClosedException;
|
||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryCommentNotOwnedException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/djb/inquiry")
|
||||
@Secured("ROLE_INQUIRY")
|
||||
@RequiredArgsConstructor
|
||||
public class InquiryCommentController {
|
||||
|
||||
private final InquiryCommentFacade inquiryCommentFacade;
|
||||
|
||||
@GetMapping("/{inquiryId}/comments")
|
||||
public ResponseEntity<List<InquiryCommentDTO>> list(@PathVariable String inquiryId) {
|
||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
||||
return ResponseEntity.ok(inquiryCommentFacade.getComments(inquiryId, current));
|
||||
}
|
||||
|
||||
@PostMapping("/{inquiryId}/comments")
|
||||
public ResponseEntity<InquiryCommentDTO> create(@PathVariable String inquiryId,
|
||||
@Valid @RequestBody InquiryCommentCreateRequest request) {
|
||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
||||
InquiryCommentDTO created = inquiryCommentFacade.createUserComment(inquiryId, request.getContent(), current);
|
||||
return ResponseEntity.ok(created);
|
||||
}
|
||||
|
||||
@DeleteMapping("/comments/{commentId}")
|
||||
public ResponseEntity<Void> delete(@PathVariable String commentId) {
|
||||
PortalAuthenticatedUser current = SecurityUtil.getPortalAuthenticatedUser();
|
||||
inquiryCommentFacade.deleteOwnComment(commentId, current);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@ExceptionHandler(InquiryClosedException.class)
|
||||
public ResponseEntity<Map<String, String>> handleClosed(InquiryClosedException ex) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(errorBody("INQUIRY_CLOSED", ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(InquiryCommentNotOwnedException.class)
|
||||
public ResponseEntity<Map<String, String>> handleNotOwned(InquiryCommentNotOwnedException ex) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(errorBody("COMMENT_NOT_OWNED", ex.getMessage()));
|
||||
}
|
||||
|
||||
private Map<String, String> errorBody(String code, String message) {
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("code", code);
|
||||
body.put("message", message);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.NotBlank;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
@Data
|
||||
public class InquiryCommentCreateRequest {
|
||||
|
||||
@NotBlank(message = "댓글 내용을 입력해주세요.")
|
||||
@Size(max = 2000, message = "댓글은 2000자를 초과할 수 없습니다.")
|
||||
private String content;
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class InquiryCommentDTO {
|
||||
|
||||
private String id;
|
||||
|
||||
private String content;
|
||||
|
||||
private String writerId;
|
||||
|
||||
private String writerName;
|
||||
|
||||
private String adminYn;
|
||||
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
private boolean deletable;
|
||||
}
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.repository;
|
||||
|
||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public interface InquiryCommentRepository extends JpaRepository<InquiryComment, String> {
|
||||
|
||||
List<InquiryComment> findByInquiry_IdAndDelYnOrderByCreatedDateAsc(String inquiryId, String delYn);
|
||||
|
||||
@Query("select c.inquiry.id, count(c) from InquiryComment c"
|
||||
+ " where c.inquiry.id in :inquiryIds and c.delYn = :delYn"
|
||||
+ " group by c.inquiry.id")
|
||||
List<Object[]> countActiveGroupByInquiry(@Param("inquiryIds") Collection<String> inquiryIds,
|
||||
@Param("delYn") String delYn);
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.repository;
|
||||
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import com.eactive.eai.data.jpa.BaseRepository;
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@EMSDataSource
|
||||
public interface UserInfoRepository extends BaseRepository<UserInfo, String> {
|
||||
|
||||
/**
|
||||
* TSEAIRM02.roleidnfiname 컬럼은 콤마로 구분된 복수 역할을 저장한다.
|
||||
* (예: {@code "admin,portal-admin"}). Oracle native query로 콤마 토큰 매치.
|
||||
*/
|
||||
@Query(value = "SELECT * FROM TSEAIRM02 t"
|
||||
+ " WHERE ',' || t.ROLEIDNFINAME || ',' LIKE '%,' || :role || ',%'",
|
||||
nativeQuery = true)
|
||||
List<UserInfo> findByRoleContaining(@Param("role") String role);
|
||||
}
|
||||
-57
@@ -1,57 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
||||
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository;
|
||||
import com.eactive.apim.portal.djb.community.qna.constant.DjbAdminRole;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.template.service.MessageHandlerService;
|
||||
import com.eactive.apim.portal.template.service.MessageRecipient;
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Q&A 등록/댓글 등록 시 portal-admin 역할(TSEAIRM02)을 가진 관리자 전원에게
|
||||
* 알림 메시지를 발행한다. 발송 실패는 트랜잭션 롤백을 유발하지 않는다.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InquiryAdminNotifier {
|
||||
|
||||
private final UserInfoRepository userInfoRepository;
|
||||
private final MessageHandlerService messageHandlerService;
|
||||
|
||||
public void notifyPortalAdmins(MessageCode code, Map<String, Object> params) {
|
||||
try {
|
||||
List<UserInfo> admins = userInfoRepository.findByRoleContaining(DjbAdminRole.PORTAL_ADMIN);
|
||||
if (admins == null || admins.isEmpty()) {
|
||||
log.warn("portal-admin 역할 관리자가 없습니다 — 알림 미발송 code={}", code.name());
|
||||
return;
|
||||
}
|
||||
for (UserInfo admin : admins) {
|
||||
try {
|
||||
MessageRecipient recipient = toRecipient(admin);
|
||||
messageHandlerService.publishEvent(code, recipient, params);
|
||||
} catch (Exception e) {
|
||||
log.warn("portal-admin 개별 알림 발행 실패 — userid={}, code={}",
|
||||
admin.getUserid(), code.name(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("portal-admin 알림 발행 실패 — code={}", code.name(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageRecipient toRecipient(UserInfo admin) {
|
||||
MessageRecipient r = new MessageRecipient();
|
||||
r.setUsername(admin.getUsername());
|
||||
r.setUserId(admin.getEmad());
|
||||
r.setPhone(admin.getCphnno());
|
||||
r.setMessengerId(admin.getUserid());
|
||||
return r;
|
||||
}
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
||||
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface InquiryCommentFacade {
|
||||
|
||||
List<InquiryCommentDTO> getComments(String inquiryId, PortalAuthenticatedUser current);
|
||||
|
||||
InquiryCommentDTO createUserComment(String inquiryId, String content, PortalAuthenticatedUser current);
|
||||
|
||||
void deleteOwnComment(String commentId, PortalAuthenticatedUser current);
|
||||
|
||||
Map<String, Long> countActiveByInquiryIds(Collection<String> inquiryIds);
|
||||
}
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
||||
|
||||
import com.eactive.apim.portal.apps.community.qna.service.InquiryService;
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.dto.InquiryCommentDTO;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.UserInfoRepository;
|
||||
import com.eactive.apim.portal.djb.community.qna.constant.DjbInquiryStatus;
|
||||
import com.eactive.apim.portal.djb.community.qna.support.InquiryCommentPermissionChecker;
|
||||
import com.eactive.apim.portal.portaluser.entity.PortalUser;
|
||||
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
||||
import com.eactive.apim.portal.template.entity.MessageCode;
|
||||
import com.eactive.apim.portal.user.entity.UserInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class InquiryCommentFacadeImpl implements InquiryCommentFacade {
|
||||
|
||||
private static final String ADMIN_N = "N";
|
||||
private static final String DEL_N = "N";
|
||||
|
||||
private final InquiryService inquiryService;
|
||||
private final InquiryCommentService commentService;
|
||||
private final InquiryCommentRepository commentRepository;
|
||||
private final InquiryCommentPermissionChecker permissionChecker;
|
||||
private final InquiryAdminNotifier adminNotifier;
|
||||
private final PortalUserRepository portalUserRepository;
|
||||
private final UserInfoRepository userInfoRepository;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public List<InquiryCommentDTO> getComments(String inquiryId, PortalAuthenticatedUser current) {
|
||||
inquiryService.getAccessibleInquiry(current, inquiryId);
|
||||
List<InquiryComment> comments = commentService.findActiveByInquiryId(inquiryId);
|
||||
Map<String, String> writerNames = resolveWriterNames(comments);
|
||||
return comments.stream()
|
||||
.map(c -> toDto(c, current, writerNames))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InquiryCommentDTO createUserComment(String inquiryId, String content, PortalAuthenticatedUser current) {
|
||||
Inquiry inquiry = inquiryService.getAccessibleInquiry(current, inquiryId);
|
||||
permissionChecker.assertWritable(inquiry);
|
||||
InquiryComment saved = commentService.create(inquiry, content, ADMIN_N);
|
||||
publishAdminNotification(inquiry, saved, current);
|
||||
Map<String, String> writerNames = new HashMap<>();
|
||||
writerNames.put(current.getId(), current.getUserName());
|
||||
return toDto(saved, current, writerNames);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public Map<String, Long> countActiveByInquiryIds(Collection<String> inquiryIds) {
|
||||
if (inquiryIds == null || inquiryIds.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
List<Object[]> rows = commentRepository.countActiveGroupByInquiry(inquiryIds, DEL_N);
|
||||
Map<String, Long> result = new HashMap<>();
|
||||
for (Object[] row : rows) {
|
||||
String inquiryId = (String) row[0];
|
||||
Long count = ((Number) row[1]).longValue();
|
||||
result.put(inquiryId, count);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteOwnComment(String commentId, PortalAuthenticatedUser current) {
|
||||
InquiryComment comment = commentService.findById(commentId)
|
||||
.orElseThrow(() -> new NotFoundException("댓글을 찾을 수 없습니다. id=" + commentId));
|
||||
inquiryService.getAccessibleInquiry(current, comment.getInquiry().getId());
|
||||
permissionChecker.assertDeletable(comment, current);
|
||||
commentService.softDelete(commentId);
|
||||
}
|
||||
|
||||
private void publishAdminNotification(Inquiry inquiry, InquiryComment comment, PortalAuthenticatedUser current) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("inquiryId", inquiry.getId());
|
||||
params.put("inquirySubject", inquiry.getInquirySubject());
|
||||
params.put("commentContent", comment.getCommentDetail());
|
||||
params.put("writerName", current.getUserName());
|
||||
adminNotifier.notifyPortalAdmins(MessageCode.INQUIRY_COMMENT_CREATED, params);
|
||||
}
|
||||
|
||||
private Map<String, String> resolveWriterNames(List<InquiryComment> comments) {
|
||||
Set<String> ids = new HashSet<>();
|
||||
for (InquiryComment c : comments) {
|
||||
String createdBy = c.getCreatedBy();
|
||||
if (createdBy != null && !createdBy.isEmpty()) {
|
||||
ids.add(createdBy);
|
||||
}
|
||||
}
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (String id : ids) {
|
||||
String name = lookupWriterName(id);
|
||||
if (name != null && !name.isEmpty()) {
|
||||
map.put(id, name);
|
||||
} else {
|
||||
log.warn("댓글 작성자명 조회 실패 — createdBy={}, isUuid={}", id, isUuid(id));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private String lookupWriterName(String createdBy) {
|
||||
if (isUuid(createdBy)) {
|
||||
String name = portalUserRepository.findById(createdBy)
|
||||
.map(PortalUser::getUserName).orElse(null);
|
||||
if (name != null && !name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return userInfoRepository.findById(createdBy)
|
||||
.map(UserInfo::getUsername).orElse(null);
|
||||
}
|
||||
String name = userInfoRepository.findById(createdBy)
|
||||
.map(UserInfo::getUsername).orElse(null);
|
||||
if (name != null && !name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
return portalUserRepository.findById(createdBy)
|
||||
.map(PortalUser::getUserName).orElse(null);
|
||||
}
|
||||
|
||||
private boolean isUuid(String value) {
|
||||
if (value == null || value.length() != 36) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
UUID.fromString(value);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private InquiryCommentDTO toDto(InquiryComment entity, PortalAuthenticatedUser current,
|
||||
Map<String, String> writerNames) {
|
||||
String writerId = entity.getCreatedBy();
|
||||
boolean deletable = writerId != null
|
||||
&& writerId.equals(current.getId())
|
||||
&& entity.getInquiry() != null
|
||||
&& !DjbInquiryStatus.isClosed(entity.getInquiry().getInquiryStatus());
|
||||
String writerName = writerId != null ? writerNames.get(writerId) : null;
|
||||
if (writerName == null || writerName.isEmpty()) {
|
||||
writerName = "(알 수 없음)";
|
||||
}
|
||||
return InquiryCommentDTO.builder()
|
||||
.id(entity.getId())
|
||||
.content(entity.getCommentDetail())
|
||||
.writerId(writerId)
|
||||
.writerName(writerName)
|
||||
.adminYn(entity.getAdminYn())
|
||||
.createdDate(entity.getCreatedDate())
|
||||
.deletable(deletable)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.comment.service;
|
||||
|
||||
import com.eactive.apim.portal.common.exception.NotFoundException;
|
||||
import com.eactive.apim.portal.djb.community.qna.comment.repository.InquiryCommentRepository;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
@RequiredArgsConstructor
|
||||
public class InquiryCommentService {
|
||||
|
||||
private static final String DEL_N = "N";
|
||||
|
||||
private final InquiryCommentRepository repository;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<InquiryComment> findActiveByInquiryId(String inquiryId) {
|
||||
return repository.findByInquiry_IdAndDelYnOrderByCreatedDateAsc(inquiryId, DEL_N);
|
||||
}
|
||||
|
||||
public InquiryComment create(Inquiry inquiry, String content, String adminYn) {
|
||||
InquiryComment comment = new InquiryComment();
|
||||
comment.setInquiry(inquiry);
|
||||
comment.setCommentDetail(content);
|
||||
comment.setAdminYn(adminYn);
|
||||
comment.setDelYn(DEL_N);
|
||||
return repository.save(comment);
|
||||
}
|
||||
|
||||
public Optional<InquiryComment> findById(String commentId) {
|
||||
return repository.findById(commentId);
|
||||
}
|
||||
|
||||
public void softDelete(String commentId) {
|
||||
InquiryComment comment = repository.findById(commentId)
|
||||
.orElseThrow(() -> new NotFoundException("댓글을 찾을 수 없습니다. id=" + commentId));
|
||||
comment.markDeleted();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.constant;
|
||||
|
||||
public final class DjbAdminRole {
|
||||
|
||||
/** TSEAIRM02.roleidnfiname 컬럼에서 포털 관리자를 식별하는 값. */
|
||||
public static final String PORTAL_ADMIN = "portal-admin";
|
||||
|
||||
private DjbAdminRole() {
|
||||
}
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.constant;
|
||||
|
||||
public final class DjbInquiryStatus {
|
||||
|
||||
public static final String PENDING = "PENDING";
|
||||
public static final String RESPONDED = "RESPONDED";
|
||||
public static final String CLOSED = "CLOSED";
|
||||
|
||||
private DjbInquiryStatus() {
|
||||
}
|
||||
|
||||
public static boolean isClosed(String status) {
|
||||
return CLOSED.equals(status);
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.exception;
|
||||
|
||||
public class InquiryClosedException extends RuntimeException {
|
||||
|
||||
public InquiryClosedException() {
|
||||
super("종료된 문의에는 댓글을 작성/삭제할 수 없습니다.");
|
||||
}
|
||||
|
||||
public InquiryClosedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.exception;
|
||||
|
||||
public class InquiryCommentNotOwnedException extends RuntimeException {
|
||||
|
||||
public InquiryCommentNotOwnedException() {
|
||||
super("본인이 작성한 댓글만 삭제할 수 있습니다.");
|
||||
}
|
||||
|
||||
public InquiryCommentNotOwnedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.support;
|
||||
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.djb.community.qna.constant.DjbInquiryStatus;
|
||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryClosedException;
|
||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryCommentNotOwnedException;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class InquiryCommentPermissionChecker {
|
||||
|
||||
public boolean canWrite(Inquiry inquiry) {
|
||||
return inquiry != null && !DjbInquiryStatus.isClosed(inquiry.getInquiryStatus());
|
||||
}
|
||||
|
||||
public boolean canDelete(InquiryComment comment, PortalAuthenticatedUser current) {
|
||||
if (comment == null || current == null) {
|
||||
return false;
|
||||
}
|
||||
boolean owner = comment.getCreatedBy() != null
|
||||
&& comment.getCreatedBy().equals(current.getId());
|
||||
boolean notClosed = comment.getInquiry() == null
|
||||
|| !DjbInquiryStatus.isClosed(comment.getInquiry().getInquiryStatus());
|
||||
return owner && notClosed;
|
||||
}
|
||||
|
||||
public void assertWritable(Inquiry inquiry) {
|
||||
if (!canWrite(inquiry)) {
|
||||
throw new InquiryClosedException();
|
||||
}
|
||||
}
|
||||
|
||||
public void assertDeletable(InquiryComment comment, PortalAuthenticatedUser current) {
|
||||
if (comment.getInquiry() != null
|
||||
&& DjbInquiryStatus.isClosed(comment.getInquiry().getInquiryStatus())) {
|
||||
throw new InquiryClosedException();
|
||||
}
|
||||
if (comment.getCreatedBy() == null
|
||||
|| !comment.getCreatedBy().equals(current.getId())) {
|
||||
throw new InquiryCommentNotOwnedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,18 @@
|
||||
</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">
|
||||
@@ -54,6 +66,10 @@
|
||||
</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>
|
||||
@@ -80,4 +96,51 @@
|
||||
<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.
@@ -1,180 +0,0 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const section = document.querySelector('.djb-comment-section');
|
||||
if (!section) return;
|
||||
|
||||
const inquiryId = section.getAttribute('data-inquiry-id');
|
||||
const closed = section.getAttribute('data-closed') === 'true';
|
||||
const listEl = document.getElementById('djbCommentList');
|
||||
const emptyEl = document.getElementById('djbCommentEmpty');
|
||||
const countEl = document.getElementById('djbCommentCount');
|
||||
const formEl = document.getElementById('djbCommentForm');
|
||||
const inputEl = document.getElementById('djbCommentInput');
|
||||
const counterEl = document.getElementById('djbCommentCharCounter');
|
||||
|
||||
function getCsrfToken() {
|
||||
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : '';
|
||||
}
|
||||
|
||||
function fetchJson(url, options) {
|
||||
options = options || {};
|
||||
const headers = Object.assign({}, options.headers || {});
|
||||
const method = (options.method || 'GET').toUpperCase();
|
||||
if (method !== 'GET' && method !== 'HEAD') {
|
||||
headers['X-XSRF-TOKEN'] = getCsrfToken();
|
||||
}
|
||||
if (options.body && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json;charset=UTF-8';
|
||||
}
|
||||
return fetch(url, Object.assign({ credentials: 'same-origin' }, options, { headers: headers }));
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return '';
|
||||
try {
|
||||
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())
|
||||
+ ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function render(comments) {
|
||||
listEl.innerHTML = '';
|
||||
if (!comments || comments.length === 0) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'djb-comment-empty';
|
||||
li.textContent = '아직 등록된 댓글이 없습니다.';
|
||||
listEl.appendChild(li);
|
||||
return;
|
||||
}
|
||||
comments.forEach(function (c) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'djb-comment-item' + (c.adminYn === 'Y' ? ' djb-comment-item--admin' : '');
|
||||
li.setAttribute('data-comment-id', c.id);
|
||||
li.innerHTML = ''
|
||||
+ '<div class="djb-comment-meta">'
|
||||
+ ' <div class="djb-comment-meta-left">'
|
||||
+ ' <span class="djb-comment-writer">' + escapeHtml(c.writerName || '(알 수 없음)') + '</span>'
|
||||
+ (c.adminYn === 'Y' ? ' <span class="djb-comment-admin-badge">관리자</span>' : '')
|
||||
+ ' </div>'
|
||||
+ ' <div class="djb-comment-meta-right">'
|
||||
+ ' <span class="djb-comment-date">' + escapeHtml(formatDate(c.createdDate)) + '</span>'
|
||||
+ (c.deletable ? ' <button type="button" class="djb-comment-delete-btn" data-id="' + escapeHtml(c.id) + '">삭제</button>' : '')
|
||||
+ ' </div>'
|
||||
+ '</div>'
|
||||
+ '<div class="djb-comment-body">' + escapeHtml(c.content) + '</div>';
|
||||
listEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function load() {
|
||||
fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments')
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error('댓글 조회에 실패했습니다.');
|
||||
return res.json();
|
||||
})
|
||||
.then(render)
|
||||
.catch(function (err) {
|
||||
console.error('[djb-comments] load error', err);
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
const content = (inputEl.value || '').trim();
|
||||
if (!content) {
|
||||
if (window.customPopups) window.customPopups.showAlert('댓글 내용을 입력해 주세요.');
|
||||
inputEl.focus();
|
||||
return;
|
||||
}
|
||||
fetchJson('/djb/inquiry/' + encodeURIComponent(inquiryId) + '/comments', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ content: content })
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res.status === 409) throw new Error('종료된 문의에는 댓글을 작성할 수 없습니다.');
|
||||
if (!res.ok) throw new Error('댓글 등록에 실패했습니다.');
|
||||
return res.json();
|
||||
})
|
||||
.then(function () {
|
||||
inputEl.value = '';
|
||||
updateCounter();
|
||||
load();
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (window.customPopups) window.customPopups.showAlert(err.message);
|
||||
else alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function handleDelete(commentId) {
|
||||
if (!commentId) return;
|
||||
function doDelete() {
|
||||
fetchJson('/djb/inquiry/comments/' + encodeURIComponent(commentId), { method: 'DELETE' })
|
||||
.then(function (res) {
|
||||
if (res.status === 403) throw new Error('본인이 작성한 댓글만 삭제할 수 있습니다.');
|
||||
if (res.status === 409) throw new Error('종료된 문의에는 댓글을 삭제할 수 없습니다.');
|
||||
if (!res.ok && res.status !== 204) throw new Error('댓글 삭제에 실패했습니다.');
|
||||
load();
|
||||
})
|
||||
.catch(function (err) {
|
||||
if (window.customPopups) window.customPopups.showAlert(err.message);
|
||||
else alert(err.message);
|
||||
});
|
||||
}
|
||||
if (window.customPopups) {
|
||||
window.customPopups.showConfirm('댓글을 삭제하시겠습니까?', function (ok) {
|
||||
if (ok) doDelete();
|
||||
});
|
||||
} else if (window.confirm('댓글을 삭제하시겠습니까?')) {
|
||||
doDelete();
|
||||
}
|
||||
}
|
||||
|
||||
function updateCounter() {
|
||||
if (!counterEl || !inputEl) return;
|
||||
const max = inputEl.getAttribute('maxlength') || '2000';
|
||||
counterEl.textContent = (inputEl.value || '').length + ' / ' + max;
|
||||
}
|
||||
|
||||
if (!closed && formEl) {
|
||||
formEl.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
});
|
||||
inputEl.addEventListener('input', updateCounter);
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
listEl.addEventListener('click', function (e) {
|
||||
const target = e.target;
|
||||
if (target && target.classList.contains('djb-comment-delete-btn')) {
|
||||
handleDelete(target.getAttribute('data-id'));
|
||||
}
|
||||
});
|
||||
|
||||
load();
|
||||
})();
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
// Color Palette - Vibrant and Modern
|
||||
// Primary colors
|
||||
$primary-blue: #0049b4; // Vibrant blue
|
||||
$primary-blue: #0049b4; // 광주은행 블루
|
||||
$secondary-blue: #c3dfea; // Deep blue
|
||||
$accent-cyan: #00D4FF; // Sky blue accent
|
||||
$accent-yellow: #FFD93D; // Yellow accent
|
||||
@@ -35,8 +35,7 @@ $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: '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-primary: '원신한', '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,97 +1,130 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// 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)
|
||||
// -----------------------------------------------------------------------------
|
||||
@use '../abstracts/variables' as *;
|
||||
@use '../abstracts/color-functions' as *;
|
||||
@use '../abstracts/mixins' as *;
|
||||
|
||||
/* OneShinhan */
|
||||
@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 */
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Light'),
|
||||
src: local('원신한 Light'),
|
||||
local('OneShinhan Light'),
|
||||
local('OneShinhan-Light'),
|
||||
url('/font/shinhan/OneShinhanLight.woff') format('woff');
|
||||
url('/font/djb/OneShinhanLight.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Medium'),
|
||||
src: local('원신한 Medium'),
|
||||
local('OneShinhan Medium'),
|
||||
local('OneShinhan-Medium'),
|
||||
url('/font/shinhan/OneShinhanMedium.woff') format('woff');
|
||||
url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Medium'),
|
||||
src: local('원신한 Medium'),
|
||||
local('OneShinhan Medium'),
|
||||
local('OneShinhan-Medium'),
|
||||
url('/font/shinhan/OneShinhanMedium.woff') format('woff');
|
||||
url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'OneShinhan';
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
src: local('OneShinhan Bold'),
|
||||
src: local('원신한 Bold'),
|
||||
local('OneShinhan Bold'),
|
||||
local('OneShinhan-Bold'),
|
||||
url('/font/shinhan/OneShinhanBold.woff') format('woff');
|
||||
url('/font/djb/OneShinhanBold.woff') format('woff');
|
||||
}
|
||||
|
||||
/* Spoqa Han Sans Neo */
|
||||
/* 한글 family name alias (변수에서 '원신한' 우선 매칭 시 동일 폰트 사용) */
|
||||
@font-face {
|
||||
font-family: 'Spoqa Han Sans Neo';
|
||||
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;
|
||||
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');
|
||||
src: url('/font/djb/OneShinhanMedium.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Spoqa Han Sans Neo';
|
||||
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;
|
||||
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');
|
||||
src: url('/font/djb/OneShinhanBold.woff') format('woff');
|
||||
}
|
||||
|
||||
/* SpoqaHanSans (Neo 미설치 환경 폴백) */
|
||||
|
||||
// KJB PIB
|
||||
/* SpoqaHanSans */
|
||||
@font-face {
|
||||
font-family: 'SpoqaHanSans';
|
||||
font-family:'SpoqaHanSans';
|
||||
font-weight: 400;
|
||||
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');
|
||||
src:url('/font/kjb/SpoqaHanSansRegular.woff') format('woff'),
|
||||
url('/font/kjb/SpoqaHanSansRegular.woff2') format('woff2'),
|
||||
url('/font/kjb/SpoqaHanSansRegular.ttf') format('truetype');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'SpoqaHanSans';
|
||||
font-family:'SpoqaHanSansBold';
|
||||
font-weight: 700;
|
||||
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');
|
||||
src:url('/font/kjb/SpoqaHanSansBold.woff') format('woff'),
|
||||
url('/font/kjb/SpoqaHanSansBold.woff2') format('woff2'),
|
||||
url('/font/kjb/SpoqaHanSansBold.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* Noto Sans KR (Variable) */
|
||||
/* HGGGothicssi */
|
||||
@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');
|
||||
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-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-face {
|
||||
font-family:'HGGGothicssi';
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -6,38 +6,26 @@
|
||||
// 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 — 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;
|
||||
}
|
||||
|
||||
// Headings
|
||||
h1, .h1 {
|
||||
font-size: $font-size-5xl;
|
||||
font-weight: $font-weight-extrabold;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
@include respond-to('sm') {
|
||||
@@ -51,6 +39,7 @@ h1, .h1 {
|
||||
|
||||
h2, .h2 {
|
||||
font-size: $font-size-4xl;
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
@include respond-to('sm') {
|
||||
@@ -64,6 +53,7 @@ h2, .h2 {
|
||||
|
||||
h3, .h3 {
|
||||
font-size: $font-size-3xl;
|
||||
font-weight: $font-weight-bold;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
@include respond-to('sm') {
|
||||
@@ -73,16 +63,19 @@ 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,7 +65,9 @@ $apikey-border-color: #dadada;
|
||||
}
|
||||
|
||||
.common-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 22px;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// DJBank Custom — Q&A 댓글 영역 (DJPGPT0002)
|
||||
// 설계서: DJPCSQ0300P 참조
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// 목록 화면 — 제목 옆 댓글 개수 표시 (설계서 ⑥)
|
||||
.inquiry-comment-count {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
color: #DC2626;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 상세 헤더 우측 그룹 (작성자 + 날짜)
|
||||
.inquiry-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #6B7280;
|
||||
|
||||
.inquiry-detail-writer {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.inquiry-detail-divider {
|
||||
color: #D1D5DB;
|
||||
}
|
||||
|
||||
.inquiry-detail-date {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.djb-comment-section {
|
||||
margin: 40px 0 48px 0;
|
||||
}
|
||||
|
||||
// ── 댓글 목록 박스 (설계서 ① 영역) ─────────────────────────
|
||||
.djb-comment-list-box {
|
||||
padding: 24px 28px;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
min-height: 120px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.djb-comment-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.djb-comment-empty {
|
||||
padding: 28px 0;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
// ── 댓글 한 건 (설계서 작성자 + 날짜 + Contents) ───────────
|
||||
.djb-comment-item {
|
||||
padding: 18px 0;
|
||||
border-bottom: 1px dashed #E5E7EB;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&--admin .djb-comment-writer {
|
||||
color: #1D4ED8;
|
||||
}
|
||||
|
||||
.djb-comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.djb-comment-meta-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.djb-comment-writer {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.djb-comment-admin-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
background-color: #DBEAFE;
|
||||
color: #1D4ED8;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.djb-comment-meta-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.djb-comment-date {
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.djb-comment-delete-btn {
|
||||
background: none;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 4px;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
color: #6B7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #FEE2E2;
|
||||
border-color: #FCA5A5;
|
||||
color: #B91C1C;
|
||||
}
|
||||
}
|
||||
|
||||
.djb-comment-body {
|
||||
padding: 12px 16px;
|
||||
background-color: #F3F4F6;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #1F2937;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 작성 영역 (설계서 ②, ③) ───────────────────────────────
|
||||
.djb-comment-form-wrapper {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.djb-comment-form {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea.djb-comment-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid #D1D5DB;
|
||||
border-radius: 8px;
|
||||
background-color: #F9FAFB;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #1F2937;
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #9CA3AF;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: #1D4ED8;
|
||||
background-color: #FFFFFF;
|
||||
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.djb-comment-form-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.djb-comment-char-counter {
|
||||
font-size: 13px;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
button.djb-comment-submit {
|
||||
display: inline-block;
|
||||
min-width: 140px;
|
||||
padding: 12px 28px;
|
||||
background-color: #1D4ED8;
|
||||
color: #FFFFFF;
|
||||
border: 1px solid #1D4ED8;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.2px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
box-shadow: 0 2px 6px rgba(29, 78, 216, 0.18);
|
||||
transition: background-color 0.15s ease, box-shadow 0.15s ease, transform 0.05s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #1E40AF;
|
||||
border-color: #1E40AF;
|
||||
box-shadow: 0 3px 10px rgba(29, 78, 216, 0.28);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #94A3B8;
|
||||
border-color: #94A3B8;
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.djb-comment-readonly-msg {
|
||||
margin: 0;
|
||||
padding: 16px 20px;
|
||||
background-color: #F3F4F6;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
color: #6B7280;
|
||||
font-size: 13px;
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #E65100;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__description {
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
@use 'components/pagination' as *;
|
||||
@use 'components/breadcrumb' as *;
|
||||
@use 'components/test-env-notice' as *;
|
||||
@use 'components/djb-inquiry-comments' as *;
|
||||
|
||||
// 5. Page-specific styles
|
||||
@use 'pages/index' as *;
|
||||
|
||||
@@ -569,7 +569,9 @@
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #212529;
|
||||
margin-bottom: 12px;
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
}
|
||||
|
||||
.app-list-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 28px;
|
||||
font-weight: $font-weight-bold;
|
||||
color: $text-dark;
|
||||
margin: 0;
|
||||
|
||||
|
||||
@@ -150,7 +150,9 @@
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 46px;
|
||||
font-weight: 700;
|
||||
line-height: 99.94%;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
@@ -498,7 +500,9 @@
|
||||
}
|
||||
|
||||
.search-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
@@ -816,7 +820,9 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 44px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: #000000;
|
||||
margin: 0;
|
||||
@@ -982,7 +988,9 @@
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.67;
|
||||
color: #2A2A2A;
|
||||
margin: 0 0 20px 0;
|
||||
@@ -1129,7 +1137,9 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -1392,7 +1402,9 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -1781,6 +1793,7 @@
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: $font-family-primary;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
|
||||
@@ -2048,7 +2061,9 @@
|
||||
}
|
||||
|
||||
.cta-title {
|
||||
font-family: $font-family-primary;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
color: #000000;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
|
||||
@@ -31,6 +31,7 @@ $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;
|
||||
@@ -38,6 +39,7 @@ $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;
|
||||
}
|
||||
@@ -64,6 +66,7 @@ $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;
|
||||
}
|
||||
|
||||
@@ -127,6 +130,7 @@ $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;
|
||||
@@ -685,6 +689,7 @@ $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,11 +384,6 @@
|
||||
&--pending {
|
||||
background-color: #8c959f;
|
||||
}
|
||||
|
||||
// 종료 - 짙은 회색 배경
|
||||
&--closed {
|
||||
background-color: #4a5560;
|
||||
}
|
||||
}
|
||||
|
||||
// Inquiry Actions
|
||||
@@ -699,25 +694,25 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 제목 컬럼 - flex 1 (인라인 min-width:200px 무력화로 모바일 축소 허용)
|
||||
// 제목 컬럼 - flex 1
|
||||
&:nth-child(2) {
|
||||
flex: 1 1 0 !important;
|
||||
min-width: 0 !important;
|
||||
flex: 1 !important;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
// 작성자 컬럼 - 60px, 가운데 정렬
|
||||
// 작성자 컬럼 - 50px, 가운데 정렬
|
||||
&:nth-child(3) {
|
||||
width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
flex: none !important;
|
||||
width: 50px !important;
|
||||
min-width: 50px;
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
// 처리상태 컬럼 - 78px, 가운데 정렬
|
||||
// 처리상태 컬럼 - 65px, 가운데 정렬
|
||||
&:nth-child(4) {
|
||||
width: 78px !important;
|
||||
min-width: 78px !important;
|
||||
flex: none !important;
|
||||
width: 65px !important;
|
||||
min-width: 65px;
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -736,12 +731,11 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 제목 컬럼 - flex 1 (인라인 min-width:200px 무력화로 모바일 축소 허용)
|
||||
// 제목 컬럼 - flex 1
|
||||
&:nth-child(2),
|
||||
&.row-cell--title {
|
||||
flex: 1 1 0 !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden;
|
||||
flex: 1 !important;
|
||||
min-width: 100px;
|
||||
justify-content: flex-start;
|
||||
|
||||
.notice-title-link {
|
||||
@@ -774,23 +768,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 작성자 컬럼 - 60px, 가운데 정렬 (마스킹 이름 넘침 방지)
|
||||
// 작성자 컬럼 - 50px, 가운데 정렬
|
||||
&:nth-child(3) {
|
||||
width: 60px !important;
|
||||
min-width: 60px !important;
|
||||
flex: none !important;
|
||||
width: 50px !important;
|
||||
min-width: 50px;
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 처리상태 컬럼 - 78px, 가운데 정렬
|
||||
// 처리상태 컬럼 - 65px, 가운데 정렬
|
||||
&:nth-child(4) {
|
||||
width: 78px !important;
|
||||
min-width: 78px !important;
|
||||
flex: none !important;
|
||||
width: 65px !important;
|
||||
min-width: 65px;
|
||||
flex: none;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
|
||||
<div th:fragment="commentSection(inquiry, commentWritable)" class="djb-comment-section"
|
||||
th:attr="data-inquiry-id=${inquiry.id},data-closed=${inquiry.inquiryStatus == 'CLOSED' ? 'true' : 'false'}">
|
||||
|
||||
<div class="djb-comment-list-box">
|
||||
<ul class="djb-comment-list" id="djbCommentList">
|
||||
<li class="djb-comment-empty" id="djbCommentEmpty">아직 등록된 댓글이 없습니다.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="djb-comment-form-wrapper" th:if="${commentWritable}">
|
||||
<form id="djbCommentForm" class="djb-comment-form" onsubmit="return false;">
|
||||
<textarea id="djbCommentInput"
|
||||
class="djb-comment-input"
|
||||
rows="4"
|
||||
maxlength="2000"
|
||||
placeholder="내용"
|
||||
required></textarea>
|
||||
<div class="djb-comment-form-actions">
|
||||
<span class="djb-comment-char-counter" id="djbCommentCharCounter">0 / 2000</span>
|
||||
<button type="submit" class="djb-comment-submit">댓글달기</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="djb-comment-form-wrapper djb-comment-readonly" th:unless="${commentWritable}">
|
||||
<p class="djb-comment-readonly-msg">종료된 문의에는 댓글을 작성할 수 없습니다.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,20 +17,30 @@
|
||||
<div class="inquiry-detail-header">
|
||||
<div class="inquiry-header-top">
|
||||
<span class="inquiry-status-badge"
|
||||
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' ? '종료' : '답변대기')}">
|
||||
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : 'inquiry-status-badge--pending'}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '답변대기'}">
|
||||
답변대기
|
||||
</span>
|
||||
<div class="inquiry-header-right">
|
||||
<span class="inquiry-detail-writer"
|
||||
th:text="${inquiry.inquirerName != null ? inquiry.inquirerName : (inquiry.inquirer != null ? inquiry.inquirer.userName : '')}">작성자</span>
|
||||
<span class="inquiry-detail-divider">|</span>
|
||||
<span class="inquiry-detail-date" th:text="${#temporals.format(inquiry.createdDate, 'yyyy.MM.dd')}">2025.11.01</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="inquiry-detail-title" th:text="${inquiry.inquirySubject}">질문 제목</h2>
|
||||
</div>
|
||||
|
||||
<!-- Attachment Section -->
|
||||
<div class="inquiry-detail-attachment" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
|
||||
<div class="attachment-list" th:with="fileInfo=${@fileService.findById(inquiry.attachFile)}">
|
||||
<div class="attachment-item" th:each="fileDetail : ${fileInfo.getFileDetails()}">
|
||||
<a th:href="'javascript:fn_downloadFile(\'' + ${fileDetail.fileId} + '\',\''+ ${fileDetail.fileSn} +'\')'" class="inquiry-attachment-link">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span class="attachment-label">첨부파일</span>
|
||||
<span class="attachment-filename">[[${fileDetail.originalFileName}]].[[${fileDetail.fileExtension}]]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inquiry Content Section -->
|
||||
<div class="inquiry-detail-content">
|
||||
<div id="inquiryDetail" class="inquiry-content-body" th:utext="${inquiry.inquiryDetail}">
|
||||
@@ -39,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Admin Response Section -->
|
||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'RESPONDED' or inquiry.inquiryStatus == 'CLOSED'}">
|
||||
<div class="inquiry-response-section" th:if="${inquiry.inquiryStatus == 'RESPONDED'}">
|
||||
<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>
|
||||
@@ -49,15 +59,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comment Section (DJBank Custom DJPGPT0002) -->
|
||||
<th:block th:replace="~{apps/community/djb/fragments-inquiry-comments :: commentSection(${inquiry}, ${commentWritable})}"></th:block>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="inquiry-detail-actions">
|
||||
<div class="actions-left">
|
||||
<button type="button" class="btn-inquiry-list" th:onclick="|location.href='@{/inquiry}'|">목록</button>
|
||||
</div>
|
||||
<div class="actions-right" th:if="${inquiry.inquiryStatus == 'PENDING' && inquiry.inquirer.id == #authentication.principal.id}">
|
||||
<div class="actions-right" th:if="${inquiry.inquiryStatus != 'RESPONDED' && inquiry.inquirer.id == #authentication.principal.id}">
|
||||
<form id="deleteForm" th:action="@{/inquiry/{id}/delete(id=${inquiry.id})}" method="post" style="display: none;"></form>
|
||||
<button type="button" class="btn-inquiry-edit" th:onclick="|location.href='@{/inquiry/edit(id=${inquiry.id})}'|">수정</button>
|
||||
<button type="button" class="btn-inquiry-delete" onclick="confirmDelete()">삭제</button>
|
||||
@@ -80,6 +87,10 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
function fn_downloadFile(fileId, fileSn) {
|
||||
window.open('[[@{/file/download}]]' + "?fileId=" + fileId + "&fileSn=" + fileSn);
|
||||
}
|
||||
|
||||
function decodeHTMLEntities(text) {
|
||||
var textArea = document.createElement('textarea');
|
||||
textArea.innerHTML = text;
|
||||
@@ -106,7 +117,6 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script th:src="@{/js/djb/inquiry-comments.js}"></script>
|
||||
</th:block>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<!-- Form Content -->
|
||||
<div class="register-form-container">
|
||||
<form name="inquiryForm" id="inquiryForm" th:action="${isNew}? @{/inquiry} : @{/inquiry/edit}"
|
||||
th:object="${inquiry}" method="post" class="register-form">
|
||||
th:object="${inquiry}" enctype="multipart/form-data" method="post" class="register-form">
|
||||
<input type="hidden" th:field="*{id}" th:if="${!isNew}">
|
||||
|
||||
<!-- Subject Field -->
|
||||
@@ -62,6 +62,53 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Field -->
|
||||
<div class="form-row">
|
||||
<div class="form-label-wrapper label-offset">
|
||||
<span class="form-label-text">첨부파일</span>
|
||||
</div>
|
||||
<div class="form-field-wrapper">
|
||||
<div class="file-upload-inline">
|
||||
<div class="file-input-display" id="fileInputDisplay">
|
||||
<span class="file-display-text" id="fileDisplayText">선택된 파일이 없습니다</span>
|
||||
<button type="button" class="btn-remove-file-inline" id="btnRemoveFile" title="파일 삭제" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="btn-file-attach" onclick="document.getElementById('file').click()">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="20" height="20" fill="url(#pattern0_1161_9889)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0_1161_9889" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_1161_9889" transform="scale(0.0217391)"/>
|
||||
</pattern>
|
||||
<image id="image0_1161_9889" width="46" height="46" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAYAAABXuSs3AAAACXBIWXMAABcSAAAXEgFnn9JSAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeRJREFUeNrsme1tgzAQhiHqAGxQRiATlGxAJ6i7QTpByQTtBrBBRiCdADYInaB0AvcsnaUTNR82GPzDr2Q5ODh6cO7LJgwWiHMeQZfgZReGYRO4LAEMreD/dYeWuQodQ/vh4ypcBK8JYC4eBMczXHGpzCXojICdB0xIwtcugX9IWx65h8kns8nyMAOWQRfjZUbG84Epj+QeYevfA/eJKPRpK3LU3K4KG+AV30ZG8OEAtEgq1Llu0L5m/qYwFYafS4WpvBDTkyrBbF7XWG1GVqTWnJuSuanGP6m18oeBcboi3UZBi+nAH3aOsCKqNCbwe4P/QjuZwB8MQiTDqnANPUE7o/Mbmw0FzInTVGT8OuVIC5xTJbaWqUQK59WVjrPHxinfgi69RegrGfluP3DcJT2PZWzoUtejirE8uAe3BN5ORIR+CGtcAZelbaJKDphR3yU0RJDOCXAAKckqFphhI5kxoavIil/22BQrU77cZNg8V+mVBPlqzokJ5KgojGQ6f1tlN2MjcwKYcNITHgbJ1sL4bSurWJTy8QFaH8c9uAf34B7cg3twD74xOK0/4hWP3OYonlUnDdTEYvK9V642G0FT8KP222ryhm0vVUt2QnvBX6fMM5wBHwfLDjl11WKdP6o/AQYA6KS7NOFJR28AAAAASUVORK5CYII="/>
|
||||
</defs>
|
||||
</svg>
|
||||
파일첨부
|
||||
</button>
|
||||
<input type="hidden" th:field="*{attachFile}">
|
||||
<input type="file"
|
||||
id="file"
|
||||
name="files"
|
||||
th:accept="${isInternalUser ? '*' : '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.hwp,.gif,.jpg,.jpeg,.png'}"
|
||||
style="display: none;">
|
||||
</div>
|
||||
<div class="form-help-text">
|
||||
<p><i class="fas fa-check-circle"></i>
|
||||
<span th:text="'첨부파일은 ' + ${@portalProperties.file.maxSize} + ' 이내로 등록해 주세요.'">첨부파일은 8MB 이내로 등록해 주세요.</span>
|
||||
</p>
|
||||
<p>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
문서파일과 이미지 파일만 등록 가능합니다. (pdf, doc, docx, xls, xlsx, ppt, pptx, hwp, gif, jpg, png)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -87,6 +134,11 @@
|
||||
<script th:inline="javascript">
|
||||
$(function () {
|
||||
const btnSubmit = document.querySelector('.btn-submit-form');
|
||||
const fileInput = document.getElementById('file');
|
||||
const fileDisplayText = document.getElementById('fileDisplayText');
|
||||
const fileInputDisplay = document.getElementById('fileInputDisplay');
|
||||
const btnRemoveFile = document.getElementById('btnRemoveFile');
|
||||
const attachFileInput = document.querySelector('input[name="attachFile"]');
|
||||
const inquiryDetailTextarea = document.getElementById('inquiryDetail');
|
||||
const charCounter = document.querySelector('.char-counter');
|
||||
|
||||
@@ -96,11 +148,13 @@
|
||||
const maxLength = inquiryDetailTextarea.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
// Character counter for textarea
|
||||
inquiryDetailTextarea.addEventListener('input', function() {
|
||||
const currentLength = this.value.length;
|
||||
const maxLength = this.getAttribute('maxlength');
|
||||
charCounter.textContent = currentLength + ' / ' + maxLength;
|
||||
|
||||
// Change color when near limit
|
||||
if (currentLength > maxLength * 0.9) {
|
||||
charCounter.style.color = '#FF6B6B';
|
||||
} else {
|
||||
@@ -109,6 +163,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Show existing file if present
|
||||
const existingFile = /*[[${!#strings.isEmpty(inquiry.attachFile) ? @fileService.findById(inquiry.attachFile).fileDetails[0].originalFileName + '.' + @fileService.findById(inquiry.attachFile).fileDetails[0].fileExtension : ''}]]*/ '';
|
||||
if (existingFile) {
|
||||
fileDisplayText.textContent = existingFile;
|
||||
fileDisplayText.classList.add('has-file');
|
||||
fileInputDisplay.classList.add('has-file');
|
||||
btnRemoveFile.style.display = 'flex';
|
||||
}
|
||||
|
||||
// Form submission handler
|
||||
btnSubmit.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
@@ -117,6 +180,7 @@
|
||||
const inquirySubject = document.getElementById('inquirySubject').value.trim();
|
||||
const inquiryDetail = document.getElementById('inquiryDetail').value;
|
||||
|
||||
// Validation
|
||||
if (!inquirySubject) {
|
||||
customPopups.showAlert('제목을 작성해 주세요.');
|
||||
document.getElementById('inquirySubject').focus();
|
||||
@@ -131,6 +195,47 @@
|
||||
form.submit();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', function () {
|
||||
const file = fileInput.files[0];
|
||||
|
||||
if (file) {
|
||||
/*[# th:if="${!isInternalUser}"]*/
|
||||
const allowedExtensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.hwp', '.gif', '.jpg', '.jpeg', '.png'];
|
||||
const fileExt = '.' + file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (!allowedExtensions.includes(fileExt)) {
|
||||
customPopups.showAlert("허용된 파일 형식이 아닙니다.\n문서파일(pdf, doc, docx, xls, xlsx, ppt, pptx, hwp)과 이미지 파일(gif, jpg, png)만 등록 가능합니다.");
|
||||
fileInput.value = '';
|
||||
return;
|
||||
}
|
||||
/*[/]*/
|
||||
|
||||
fileDisplayText.textContent = file.name;
|
||||
fileDisplayText.classList.add('has-file');
|
||||
fileInputDisplay.classList.add('has-file');
|
||||
btnRemoveFile.style.display = 'flex';
|
||||
|
||||
if (attachFileInput) {
|
||||
attachFileInput.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// File remove handler
|
||||
btnRemoveFile.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
fileInput.value = '';
|
||||
fileDisplayText.textContent = '선택된 파일이 없습니다';
|
||||
fileDisplayText.classList.remove('has-file');
|
||||
fileInputDisplay.classList.remove('has-file');
|
||||
btnRemoveFile.style.display = 'none';
|
||||
|
||||
// Clear attachFile hidden input
|
||||
if (attachFileInput) {
|
||||
attachFileInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Focus on subject field
|
||||
document.getElementById('inquirySubject').focus();
|
||||
});
|
||||
|
||||
@@ -62,16 +62,18 @@
|
||||
<a th:href="@{/inquiry/detail(id=${inquiry.id})}" class="notice-title-link">
|
||||
<span class="notice-number" th:text="|[${page.totalElements - (page.number * page.size) - status.index}]|">[1]</span>
|
||||
<span th:text="${inquiry.inquirySubject}">질문 제목</span>
|
||||
<span class="inquiry-comment-count"
|
||||
th:if="${commentCounts != null and commentCounts.get(inquiry.id) != null and commentCounts.get(inquiry.id) > 0}"
|
||||
th:text="|[${commentCounts.get(inquiry.id)}]|">[3]</span>
|
||||
<span class="file-icon" th:if="${!#strings.isEmpty(inquiry.attachFile)}">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 15.75C18 17.4833 17.3917 18.9583 16.175 20.175C14.9583 21.3917 13.4833 22 11.75 22C10.0167 22 8.54167 21.3917 7.325 20.175C6.10833 18.9583 5.5 17.4833 5.5 15.75V6.5C5.5 5.25 5.9375 4.1875 6.8125 3.3125C7.6875 2.4375 8.75 2 10 2C11.25 2 12.3125 2.4375 13.1875 3.3125C14.0625 4.1875 14.5 5.25 14.5 6.5V15.25C14.5 16.0167 14.2333 16.6667 13.7 17.2C13.1667 17.7333 12.5167 18 11.75 18C10.9833 18 10.3333 17.7333 9.8 17.2C9.26667 16.6667 9 16.0167 9 15.25V6H11V15.25C11 15.4667 11.0708 15.6458 11.2125 15.7875C11.3542 15.9292 11.5333 16 11.75 16C11.9667 16 12.1458 15.9292 12.2875 15.7875C12.4292 15.6458 12.5 15.4667 12.5 15.25V6.5C12.4833 5.8 12.2375 5.20833 11.7625 4.725C11.2875 4.24167 10.7 4 10 4C9.3 4 8.70833 4.24167 8.225 4.725C7.74167 5.20833 7.5 5.8 7.5 6.5V15.75C7.48333 16.9333 7.89167 17.9375 8.725 18.7625C9.55833 19.5875 10.5667 20 11.75 20C12.9167 20 13.9083 19.5875 14.725 18.7625C15.5417 17.9375 15.9667 16.9333 16 15.75V6H18V15.75Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<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 == 'RESPONDED' ? 'inquiry-status-badge--completed' : (inquiry.inquiryStatus == 'CLOSED' ? 'inquiry-status-badge--closed' : 'inquiry-status-badge--pending')}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : (inquiry.inquiryStatus == 'CLOSED' ? '종료' : '답변대기')}">
|
||||
th:classappend="${inquiry.inquiryStatus == 'RESPONDED' ? 'inquiry-status-badge--completed' : 'inquiry-status-badge--pending'}"
|
||||
th:text="${inquiry.inquiryStatus == 'RESPONDED' ? '답변완료' : '답변대기'}">
|
||||
답변대기
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -15,50 +15,10 @@
|
||||
|
||||
<!-- Notice Header: Title and Date -->
|
||||
<div class="notice-detail-header">
|
||||
<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>
|
||||
<h2 class="notice-detail-title" th:text="${portalNotice.noticeSubject}">개인정보처리 방침 정정 공지</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,15 +58,6 @@
|
||||
<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,6 +81,7 @@
|
||||
<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>
|
||||
@@ -175,7 +176,6 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
package com.eactive.apim.portal.djb.community.qna.support;
|
||||
|
||||
import com.eactive.apim.portal.common.user.PortalAuthenticatedUser;
|
||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryClosedException;
|
||||
import com.eactive.apim.portal.djb.community.qna.exception.InquiryCommentNotOwnedException;
|
||||
import com.eactive.apim.portal.qna.entity.Inquiry;
|
||||
import com.eactive.apim.portal.qna.entity.InquiryComment;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InquiryCommentPermissionCheckerTest {
|
||||
|
||||
private InquiryCommentPermissionChecker checker;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
checker = new InquiryCommentPermissionChecker();
|
||||
}
|
||||
|
||||
private Inquiry inquiry(String status) {
|
||||
Inquiry i = new Inquiry();
|
||||
i.setId("inq-1");
|
||||
i.setInquiryStatus(status);
|
||||
return i;
|
||||
}
|
||||
|
||||
private InquiryComment comment(String createdBy, Inquiry inquiry) {
|
||||
InquiryComment c = new InquiryComment();
|
||||
c.setId("cmt-1");
|
||||
c.setInquiry(inquiry);
|
||||
c.setCommentDetail("test");
|
||||
c.setAdminYn("N");
|
||||
c.setDelYn("N");
|
||||
c.setCreatedBy(createdBy);
|
||||
return c;
|
||||
}
|
||||
|
||||
private PortalAuthenticatedUser user(String id) {
|
||||
PortalAuthenticatedUser u = new PortalAuthenticatedUser();
|
||||
u.setId(id);
|
||||
u.setLoginId(id);
|
||||
return u;
|
||||
}
|
||||
|
||||
@Test
|
||||
void canWrite_pendingTrue() {
|
||||
assertTrue(checker.canWrite(inquiry("PENDING")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canWrite_inProgressTrue() {
|
||||
assertTrue(checker.canWrite(inquiry("RESPONDED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canWrite_closedFalse() {
|
||||
assertFalse(checker.canWrite(inquiry("CLOSED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canDelete_ownerInOpenTrue() {
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertTrue(checker.canDelete(c, user("alice")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canDelete_otherUserFalse() {
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertFalse(checker.canDelete(c, user("bob")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canDelete_closedFalse() {
|
||||
Inquiry i = inquiry("CLOSED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertFalse(checker.canDelete(c, user("alice")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertWritable_closedThrows() {
|
||||
assertThrows(InquiryClosedException.class, () -> checker.assertWritable(inquiry("CLOSED")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertWritable_pendingOk() {
|
||||
assertDoesNotThrow(() -> checker.assertWritable(inquiry("PENDING")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertDeletable_closedThrowsClosed() {
|
||||
Inquiry i = inquiry("CLOSED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertThrows(InquiryClosedException.class, () -> checker.assertDeletable(c, user("alice")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertDeletable_notOwnerThrowsNotOwned() {
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertThrows(InquiryCommentNotOwnedException.class,
|
||||
() -> checker.assertDeletable(c, user("bob")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertDeletable_ownerInOpenOk() {
|
||||
Inquiry i = inquiry("RESPONDED");
|
||||
InquiryComment c = comment("alice", i);
|
||||
assertDoesNotThrow(() -> checker.assertDeletable(c, user("alice")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user