초기 업로드
@@ -0,0 +1,6 @@
|
||||
.gradle/
|
||||
build/
|
||||
out/
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
@@ -0,0 +1,66 @@
|
||||
# IOTDOOR Spring Boot 마이그레이션 계획
|
||||
|
||||
## 목표
|
||||
|
||||
- 레거시 `eGovFrame + Spring MVC` 구조를 `Spring Boot 3.x + Java 17` 기반으로 전환
|
||||
- 기존 공개 사이트 디자인은 최대한 유지
|
||||
- 관리자 영역은 운영성과 보안성을 우선으로 재설계
|
||||
- 불필요한 레거시 자산과 연결되지 않은 화면은 점진적으로 제외
|
||||
|
||||
## 현재 1차 이행 범위
|
||||
|
||||
- Spring Boot 3 / Java 17 신규 모듈 생성
|
||||
- 공개 메인 페이지 이식
|
||||
- 연락처 페이지 이식
|
||||
- 포트폴리오 페이지를 정적/큐레이션 방식으로 재구성
|
||||
- 관리자 로그인/로그아웃 이식
|
||||
- 관리자 대시보드 신규 구성
|
||||
- 문의 접수를 서버 검증 API 기반으로 교체
|
||||
|
||||
## 이번 단계에서 의도적으로 보류한 범위
|
||||
|
||||
- 게시판 CRUD
|
||||
- 회원가입 및 사용자 상세 프로필
|
||||
- 메시지/알림
|
||||
- 설문
|
||||
- 관리자 CMS 전체
|
||||
- 운영 로그 및 통계
|
||||
- Hypermeta 연동
|
||||
- DB 기반 메뉴/권한 시스템
|
||||
|
||||
## 설계 원칙
|
||||
|
||||
1. 레거시와 직접 충돌하지 않도록 신규 모듈 `iotdoor-boot/` 에서 시작한다.
|
||||
2. URL은 가능한 한 기존 `.do` 패턴을 유지한다.
|
||||
3. 화면은 기존 Thymeleaf 템플릿과 정적 리소스를 재사용한다.
|
||||
4. 인증은 Spring Security 6 표준 구성으로 재작성한다.
|
||||
5. 보안 취약점으로 보였던 항목은 기본값부터 개선한다.
|
||||
|
||||
## 우선순위 제안
|
||||
|
||||
### 2차
|
||||
|
||||
- 공통 레이아웃/메뉴를 DB 비의존 구조로 정리
|
||||
- 게시판 읽기 전용 목록/상세 먼저 이식
|
||||
- 파일 업로드/다운로드 서비스 재구성
|
||||
|
||||
### 3차
|
||||
|
||||
- 관리자 게시판/공지 관리
|
||||
- 회원 조회 및 권한 관리
|
||||
- 사이트 정보/배너/팝업 관리
|
||||
|
||||
### 4차
|
||||
|
||||
- 메시지, 설문, 통계, 로그 이식
|
||||
- MyBatis 매퍼 재정비 및 DB 스키마 문서화
|
||||
- 외부 연동(Hypermeta) 재적용
|
||||
|
||||
## 보안 개선 기준
|
||||
|
||||
- CSRF 기본 활성화
|
||||
- BCrypt 비밀번호 사용
|
||||
- 관리자 계정은 환경변수로 주입
|
||||
- 클라이언트 직접 메일 발송 제거
|
||||
- 운영 민감정보의 템플릿/JS 하드코딩 최소화
|
||||
|
||||
@@ -0,0 +1,722 @@
|
||||
# IOTDOOR 프로그램 명세서
|
||||
|
||||
## 1. 문서 개요
|
||||
|
||||
- 작성일: 2026-04-15
|
||||
- 분석 대상: 현재 저장소 기준 소스코드 전체
|
||||
- 작성 목적: 프로젝트 구조, 기능 범위, 운영 방식, 배포 구조, 유지보수 유의사항을 한 문서로 정리
|
||||
- 분석 기준 파일:
|
||||
- `pom.xml`
|
||||
- `src/main/webapp/WEB-INF/web.xml`
|
||||
- `src/main/resources/egovframework/spring/com/*.xml`
|
||||
- `src/main/webapp/WEB-INF/config/egovframework/springmvc/*.xml`
|
||||
- `src/main/java/kr/iotdoor/**`
|
||||
- `src/main/resources/egovframework/mapper/kr/iotdoor/**`
|
||||
|
||||
본 문서는 저장소에 포함된 소스코드를 기준으로 작성되었으며, 저장소에 없는 DB DDL, 인프라 구성, 외부 시스템 상세 계약은 확인 가능한 범위 내에서만 기술하였다.
|
||||
|
||||
## 2. 시스템 개요
|
||||
|
||||
IOTDOOR는 `eGovFrame + Spring MVC + MyBatis + Thymeleaf` 기반의 포털형 웹 애플리케이션이다.
|
||||
단순 게시판 서비스가 아니라 다음 성격이 함께 포함된 통합형 시스템으로 판단된다.
|
||||
|
||||
- 사이트 운영/CMS 기능
|
||||
- 사이트 정보 관리
|
||||
- 메뉴 관리
|
||||
- 동적 페이지 편집
|
||||
- 팝업/배너 운영
|
||||
- 정책 페이지 관리
|
||||
- 커뮤니티 기능
|
||||
- 게시판
|
||||
- 댓글
|
||||
- FAQ
|
||||
- Q&A
|
||||
- 설문
|
||||
- 쪽지
|
||||
- 사용자 기능
|
||||
- 로그인/로그아웃
|
||||
- 회원가입
|
||||
- 개인정보/프로필 관리
|
||||
- 최근 검색어 관리
|
||||
- 운영자 기능
|
||||
- 관리자 대시보드
|
||||
- 사용자/권한/사이트/코드 관리
|
||||
- 운영 로그 및 통계 화면
|
||||
- 외부 연동 기능
|
||||
- Hypermeta API 연동
|
||||
|
||||
URI 네임스페이스는 대체로 아래처럼 구분된다.
|
||||
|
||||
| 구분 | URI 패턴 | 역할 |
|
||||
| --- | --- | --- |
|
||||
| 포털 메인 | `/`, `/index.do` | 일반 사용자 메인 진입점 |
|
||||
| 공통 기능 | `/comn/**` | 로그인, 회원, 파일, 메시지, 사이트 공통 기능 |
|
||||
| 커뮤니티 | `/cmty/**` | 게시판, 댓글, FAQ, Q&A, 설문 |
|
||||
| 관리자 | `/mfuc/**` | 운영자 화면 및 백오피스 |
|
||||
| 통계 | `/scnt/**` | 통계 대시보드 및 집계 화면 |
|
||||
|
||||
## 3. 기술 스택
|
||||
|
||||
| 항목 | 내용 |
|
||||
| --- | --- |
|
||||
| 언어/런타임 | Java 8 |
|
||||
| 빌드 도구 | Maven |
|
||||
| 패키징 | WAR |
|
||||
| 배포 형태 | `ROOT.war`로 패키징되는 톰캣 배포형 웹앱 |
|
||||
| 프레임워크 | Spring 5.3.20, eGovFrame 4.1.0 |
|
||||
| 웹 MVC | Spring MVC Annotation Controller |
|
||||
| 뷰 엔진 | Thymeleaf 3, Spring Security Dialect, Layout Dialect |
|
||||
| 호환 설정 | JSP View Resolver도 설정되어 있으나 실제 화면은 Thymeleaf 중심 |
|
||||
| 영속성 | MyBatis, Mapper Interface + SQL XML |
|
||||
| DB | MariaDB 계열 전제, 런타임은 JNDI `java:comp/env/jdbc/serviceDB` 사용 |
|
||||
| 보안 | Spring Security + eGov Security/Access |
|
||||
| JSON 처리 | Jackson |
|
||||
| 외부 통신 | OkHttp, Moshi, Apache HttpClient |
|
||||
| 파일 처리 | Commons FileUpload, Thumbnailator |
|
||||
| 스케줄링 | Quartz 의존성 포함, 일부 스케줄 설정 흔적 존재 |
|
||||
| 로깅 | Log4j2, log4jdbc |
|
||||
|
||||
## 4. 빌드 및 실행 구조
|
||||
|
||||
### 4.1 Maven 프로필
|
||||
|
||||
프로젝트는 환경별 리소스를 분리해서 사용한다.
|
||||
|
||||
- `local`
|
||||
- `dev`
|
||||
- `real`
|
||||
- `2025`
|
||||
|
||||
기본 활성 프로필은 `local`이다.
|
||||
|
||||
### 4.2 빌드 산출물
|
||||
|
||||
- 산출 디렉터리: `target/`
|
||||
- 최종 WAR 이름: `ROOT.war`
|
||||
|
||||
예시 빌드 명령:
|
||||
|
||||
- `mvn clean package -Plocal`
|
||||
- `mvn clean package -Pdev`
|
||||
- `mvn clean package -Preal`
|
||||
|
||||
### 4.3 환경별 리소스
|
||||
|
||||
환경별 프로퍼티와 로그 설정은 아래 디렉터리로 분리되어 있다.
|
||||
|
||||
- `src/main/resources-local`
|
||||
- `src/main/resources-dev`
|
||||
- `src/main/resources-real`
|
||||
|
||||
Maven 리소스 설정상 `src/main/resources-${env}` 가 선택적으로 포함된다.
|
||||
|
||||
### 4.4 웹 설정
|
||||
|
||||
`web.xml` 기준 주요 설정은 다음과 같다.
|
||||
|
||||
- 세션 타임아웃: 60분
|
||||
- 웰컴 파일: `index.jsp`
|
||||
- 에러 페이지:
|
||||
- `404 -> /code404.jsp`
|
||||
- `500 -> /code500.jsp`
|
||||
|
||||
`egov-com-servlet.xml` 기준 주요 설정은 다음과 같다.
|
||||
|
||||
- 컨트롤러 스캔 대상: `egovframework, kr`
|
||||
- 인터셉터 등록:
|
||||
- `LocaleChangeInterceptor`
|
||||
- `WebLogInterceptor`
|
||||
- `MenuPageInterceptor`
|
||||
- Thymeleaf View Resolver 사용
|
||||
- JSON View 사용
|
||||
- `/resources/**` 정적 리소스 제공
|
||||
|
||||
## 5. 시스템 아키텍처
|
||||
|
||||
### 5.1 계층 구조
|
||||
|
||||
시스템은 전형적인 계층형 구조를 따른다.
|
||||
|
||||
1. Controller
|
||||
2. Service
|
||||
3. Mapper Interface
|
||||
4. MyBatis SQL XML
|
||||
5. DB
|
||||
6. Thymeleaf Template / Static Resource
|
||||
|
||||
대표적인 요청 흐름은 아래와 같다.
|
||||
|
||||
1. 클라이언트 요청이 Spring MVC Controller로 진입
|
||||
2. 인터셉터에서 웹 로그 기록 및 메뉴 정보 주입
|
||||
3. Controller에서 인증/권한/파라미터 검증 수행
|
||||
4. Service에서 업무 로직 처리
|
||||
5. Mapper와 SQL XML을 통해 DB 접근
|
||||
6. HTML 또는 JSON 응답 반환
|
||||
|
||||
### 5.2 패키지 구조
|
||||
|
||||
| 패키지 | 역할 |
|
||||
| --- | --- |
|
||||
| `kr.iotdoor.comn` | 사이트 공통 기능, 회원, 로그인, 파일, 메시지, 메뉴, 팝업, 페이지, API |
|
||||
| `kr.iotdoor.cmty` | 게시판, 댓글, FAQ, Q&A, 설문 등 커뮤니티 기능 |
|
||||
| `kr.iotdoor.mfuc` | 관리자 기능, 권한/접근/운영/로그 관리 |
|
||||
| `kr.iotdoor.scnt` | 통계 기능 |
|
||||
| `kr.iotdoor.iotdoor` | 포털 메인 외 추가 공개 페이지 |
|
||||
|
||||
### 5.3 메뉴 기반 포털 구조
|
||||
|
||||
이 프로젝트는 하드코딩된 메뉴보다 DB 기반 메뉴 구조를 중심으로 동작한다.
|
||||
|
||||
- `MenuServiceImpl` 에서 헤더 메뉴, 사이드 메뉴, breadcrumb location 구성
|
||||
- `MenuPageInterceptor` 가 `menuSn` 값을 기준으로 `menuTree` 를 요청에 주입
|
||||
- 메뉴 수정/등록/활성화/삭제 시 메뉴 캐시를 갱신
|
||||
- 최대 4차 메뉴까지 고려한 구조가 구현되어 있음
|
||||
|
||||
### 5.4 뷰 렌더링 구조
|
||||
|
||||
- 메인 템플릿 위치:
|
||||
- `src/main/webapp/WEB-INF/template/kr/iotdoor/**`
|
||||
- 정적 리소스 위치:
|
||||
- `src/main/webapp/resources/**`
|
||||
- 동적 페이지 편집 대상:
|
||||
- HTML: `WEB-INF/template/kr/iotdoor/comn/site/page/*.html`
|
||||
- CSS/JS: `resources/kr/iotdoor/comn/site/page/**`
|
||||
|
||||
## 6. 기능 명세
|
||||
|
||||
### 6.1 포털 메인 및 공통 사이트 기능
|
||||
|
||||
#### 메인 페이지
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/`
|
||||
- `/index.do`
|
||||
- `/portfolio.do`
|
||||
- `/contact.do`
|
||||
- `/prvcPrcsPlan.do`
|
||||
- `/emlClctReject.do`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 메뉴 트리 로딩
|
||||
- 메인 공지사항 목록 노출
|
||||
- 활성 팝업 목록 노출
|
||||
- 사이트 정보 및 정책 페이지 렌더링
|
||||
|
||||
#### 관리자 메인
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/mngr/index.do`
|
||||
- `/mfuc/index.do`
|
||||
- `/mfuc/system/dashboard/siteSumryPge.do`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 로그인 여부 및 `ROLE_MANAGER` 권한 확인
|
||||
- 공통 메뉴/프로필/게시판 목록 로딩
|
||||
- 운영자 대시보드 화면 제공
|
||||
|
||||
### 6.2 로그인/회원가입/회원 정보
|
||||
|
||||
#### 로그인
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/comn/user/login/loginUser.do`
|
||||
- `/comn/user/login/actionLogin.do`
|
||||
- `/comn/user/login/main.do`
|
||||
- `/comn/user/login/logout.do`
|
||||
- `/comn/user/login/lgnLmtPge.do`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 개발 모드에서 개발용 로그인 보조값 표시 가능
|
||||
- 로그인 후 이전 URL로 복귀 처리
|
||||
- 마지막 로그인 일시 갱신
|
||||
|
||||
#### 회원가입
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/comn/user/join/mbrRegPge.do`
|
||||
- `/comn/user/join/writeJoinMber.do`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 세션의 `join_member` 정보를 기반으로 회원가입 진행
|
||||
- 기관 정보, 학년/반, 지역 코드 처리
|
||||
- 개인정보 처리 동의 여부 필수 검증
|
||||
|
||||
#### 개인정보/프로필
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/comn/user/info/indvdlInfo.do`
|
||||
- `/comn/user/info/modifyIndvdlInfo.do`
|
||||
- `/comn/user/info/modifyIndvdlInfo.json`
|
||||
- `/comn/user/info/searchUser.json`
|
||||
- `/comn/user/info/searchRcvr.json`
|
||||
- `/comn/user/info/proflPgeMdfcn.do`
|
||||
- `/comn/user/info/checkDpcnNcnm.json`
|
||||
- `/comn/user/info/uploadImage.json`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 사용자 기본 정보와 상세 정보 분리 조회
|
||||
- 거주지역 공통코드 조회
|
||||
- 별명 중복검사 후 프로필 수정
|
||||
- 프로필 이미지 업로드
|
||||
- 메시지 수신자 자동완성 검색
|
||||
|
||||
### 6.3 메시지 기능
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/comn/msg/info/rcptnMsgList.do`
|
||||
- `/comn/msg/info/dsptchMsgList.do`
|
||||
- `/comn/msg/info/msgDtl.do`
|
||||
- `/comn/msg/info/deleteRcptnMsg.do`
|
||||
- `/comn/msg/info/msgTrsm.do`
|
||||
- `/comn/msg/info/sendMsgDstch.json`
|
||||
- `/comn/msg/info/ntcnList.json`
|
||||
- `/comn/msg/info/ntcnCnt.json`
|
||||
- `/comn/msg/tmplat/selectMsgTmplat.json`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 수신/발신 메시지 목록 관리
|
||||
- 메시지 상세 확인 시 수신 여부 갱신
|
||||
- 수신자 검색 기반 쪽지 발송
|
||||
- 메시지 템플릿 기반 전송 지원
|
||||
- 알림 개수/목록 조회
|
||||
|
||||
### 6.4 파일 및 첨부파일 기능
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/comn/file/downloadFile.do`
|
||||
- `/comn/file/getImage.do`
|
||||
- `/comn/file/deleteFile.json`
|
||||
- `/comn/file/imgUpload.json`
|
||||
- `/comn/file/fileUpload.json`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 첨부파일 메타데이터는 DB에 저장
|
||||
- 실제 파일은 환경별 파일 저장 경로에 저장
|
||||
- 에디터용 이미지/파일 업로드 지원
|
||||
- 이미지 미존재 시 기본 이미지 대체 처리
|
||||
|
||||
### 6.5 게시판/댓글/신고 기능
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/cmty/bbs/pst/pstList.do`
|
||||
- `/cmty/bbs/pst/pstDtl.do`
|
||||
- `/cmty/bbs/pst/pstReg.do`
|
||||
- `/cmty/bbs/pst/pstMdfcn.do`
|
||||
- `/cmty/bbs/pst/pstReply.do`
|
||||
- `/cmty/bbs/pst/deletePst.do`
|
||||
- `/cmty/bbs/pst/download.do`
|
||||
- `/cmty/bbs/pst/pstList.json`
|
||||
|
||||
댓글 관련 URI:
|
||||
|
||||
- `/cmty/cmnt/writeCmnt.json`
|
||||
- `/cmty/cmnt/cmntList.json`
|
||||
- `/cmty/cmnt/replyCmnt.json`
|
||||
- `/cmty/cmnt/cmntDtl.json`
|
||||
- `/cmty/cmnt/modifyCmnt.json`
|
||||
- `/cmty/cmnt/deleteCmnt.json`
|
||||
- `/cmty/cmnt/cmntRcmdtn.json`
|
||||
|
||||
게시글 신고 URI:
|
||||
|
||||
- `/comn/dclr/writePstDclr.json`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 게시판별 권한 코드에 따른 조회/등록/수정/삭제/답글/다운로드/댓글 권한 통제
|
||||
- 게시판 템플릿 코드(`BT001`, `BT002`)에 따른 화면 분기
|
||||
- 공지 여부, 비공개 여부, 게시 기간 설정 지원
|
||||
- 첨부파일 업로드/다운로드 지원
|
||||
- 댓글 목록, 답글, 추천 기능 지원
|
||||
- 본문 저장 전 스크립트 제거 처리 수행
|
||||
|
||||
### 6.6 FAQ 기능
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/cmty/faq/faqList.do`
|
||||
- `/cmty/faq/faqDtl.do`
|
||||
- `/cmty/faq/faqReg.do`
|
||||
- `/cmty/faq/faqMdfcn.do`
|
||||
- `/cmty/faq/deleteFaq.do`
|
||||
|
||||
관리자 FAQ URI:
|
||||
|
||||
- `/mfuc/system/faq/faqListMng.do`
|
||||
- `/mfuc/system/faq/faqListMng.json`
|
||||
- `/mfuc/system/faq/mngrFaqReg.do`
|
||||
- `/mfuc/system/faq/faqMdfcn.do`
|
||||
- `/mfuc/system/faq/deleteFaq.json`
|
||||
|
||||
### 6.7 Q&A 기능
|
||||
|
||||
코드상 Q&A는 두 갈래로 운영된다.
|
||||
|
||||
1. 일반 사이트 Q&A
|
||||
2. 교실/수업형 Q&A
|
||||
|
||||
대표 URI:
|
||||
|
||||
- 일반 Q&A
|
||||
- `/cmty/qna/qnaList.do`
|
||||
- `/cmty/qna/qnaDtl.do`
|
||||
- `/cmty/qna/qnaReg.do`
|
||||
- `/cmty/qna/qnaMdfcn.do`
|
||||
- `/cmty/qna/deleteQstn.do`
|
||||
- `/cmty/qna/qstnAnsReg.do`
|
||||
- 수업형 Q&A
|
||||
- `/cmty/qna/clas/tcher/tcherQstnList.do`
|
||||
- `/cmty/qna/clas/stdnt/stdntSelfQstnList.do`
|
||||
- `/cmty/qna/clas/stdnt/stdntQstnReg.do`
|
||||
- `/cmty/qna/clas/stdnt/stdntPreQstnReg.do`
|
||||
- `/cmty/qna/clas/stdnt/writeStdntQstn.json`
|
||||
- `/cmty/qna/clas/tcher/modifyTcherQstnAns.json`
|
||||
- `/cmty/qna/clas/stdnt/modifyStdntQstn.json`
|
||||
- `/cmty/qna/clas/qstnDownload.do`
|
||||
- `/cmty/qna/clas/ansDownload.do`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 교사/관리자/모니터/학생 권한에 따라 접근 제어
|
||||
- 질문 첨부파일과 답변 첨부파일을 분리 관리
|
||||
- 일부 수정/등록 요청은 세션 기반 수동 토큰 검증 수행
|
||||
- 질문 유형/상태는 공통 상세코드에서 조회
|
||||
|
||||
### 6.8 설문 기능
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/cmty/srvy/srvyList.do`
|
||||
- `/cmty/srvy/srvyReg.do`
|
||||
- `/cmty/srvy/srvyMdfcn.do`
|
||||
- `/cmty/srvy/writeMngrSrvy.json`
|
||||
- `/cmty/srvy/modifyMngrSrvy.json`
|
||||
- `/cmty/srvy/deleteSrvy.json`
|
||||
- `/cmty/srvy/deleteSrvyQstn.json`
|
||||
- `/cmty/srvy/srvySbmsn.do`
|
||||
- `/cmty/srvy/insertSrvySbmsn.json`
|
||||
- `/cmty/srvy/searchSrvy.json`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 사이트 설문 목록 조회
|
||||
- 설문 등록/수정/삭제
|
||||
- 설문 질문 삭제
|
||||
- 응답자 중복 참여 방지
|
||||
- 설문 자동완성 검색 지원
|
||||
|
||||
### 6.9 사이트 운영/CMS 기능
|
||||
|
||||
#### 사이트 정보 및 정책 관리
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/mfuc/system/site/siteInfo.do`
|
||||
- `/mfuc/system/site/modifySiteInfo.do`
|
||||
- `/mfuc/system/site/modifySiteInfo.json`
|
||||
- `/comn/site/sitePolicyList.do`
|
||||
- `/comn/site/sitePolicy.do`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 사이트 상세 정보 수정
|
||||
- 도메인 목록 관리
|
||||
- 정책 변경 시 이력형 관리
|
||||
|
||||
#### 페이지 편집
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/mfuc/system/pge/pageEdit.do`
|
||||
- `/mfuc/system/pge/savePge.json`
|
||||
|
||||
주요 동작:
|
||||
|
||||
- 페이지 정보 DB 저장
|
||||
- HTML/CSS/JS 소스를 실제 파일 경로와 동기화
|
||||
- 파일이 없으면 DB 소스를 복구용 데이터로 사용
|
||||
|
||||
#### 메뉴 관리
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/mfuc/system/menu/menuListMng.do`
|
||||
- `/mfuc/system/menu/menuReg.do`
|
||||
- `/mfuc/system/menu/menuRegist.json`
|
||||
- `/mfuc/system/menu/menuMdfcn.do`
|
||||
- `/mfuc/system/menu/menuMdfcn.json`
|
||||
- `/mfuc/system/menu/menuReactvtn.do`
|
||||
|
||||
#### 팝업/배너 관리
|
||||
|
||||
주요 URI:
|
||||
|
||||
- 사용자 팝업:
|
||||
- `/comn/popup/windowPopup.do`
|
||||
- 관리자 팝업:
|
||||
- `/mfuc/system/popup/popupListMng.do`
|
||||
- `/mfuc/system/popup/popupReg.do`
|
||||
- `/mfuc/system/popup/popupMdfcn.do`
|
||||
- `/mfuc/system/popup/deletePopup.do`
|
||||
- 관리자 배너:
|
||||
- `/mfuc/system/banner/bannerListMng.do`
|
||||
- `/mfuc/system/banner/bannerReg.do`
|
||||
- `/mfuc/system/banner/bannerMdfcn.do`
|
||||
- `/mfuc/system/banner/deleteBanner.do`
|
||||
- `/mfuc/system/banner/uploadImage.json`
|
||||
|
||||
### 6.10 관리자 백오피스 기능
|
||||
|
||||
`/mfuc/system/**` 영역에서 확인되는 주요 운영 기능은 다음과 같다.
|
||||
|
||||
- 관리자 대시보드
|
||||
- 사용자 관리
|
||||
- 게시판/공지 관리
|
||||
- 댓글 관리
|
||||
- FAQ 관리
|
||||
- Q&A 관리
|
||||
- 설문 관리
|
||||
- 사이트 관리
|
||||
- 메뉴 관리
|
||||
- 페이지 관리
|
||||
- 팝업/배너 관리
|
||||
- 공통코드 관리
|
||||
- 메시지/템플릿 관리
|
||||
- 관리자 접근 IP 제한 관리
|
||||
- 권한 관련 화면
|
||||
- 운영 로그 관리
|
||||
|
||||
대표 URI 예시:
|
||||
|
||||
- `/mfuc/system/user/userListMng.do`
|
||||
- `/mfuc/system/bbs/info/mngrBbsList.do`
|
||||
- `/mfuc/system/bbs/pst/mngrBbsSntncList.do`
|
||||
- `/mfuc/system/bbs/pst/mngrNotice.do`
|
||||
- `/mfuc/system/cd/cmmnCdListMng.do`
|
||||
- `/mfuc/system/cd/cmmnDtlCdList.do`
|
||||
- `/mfuc/system/acs/mngrIpLmtMng.do`
|
||||
- `/mfuc/system/msg/msgTrsmListMng.do`
|
||||
- `/mfuc/system/msg/tmplat/msgTmplatList.do`
|
||||
|
||||
### 6.11 통계 및 로그 기능
|
||||
|
||||
통계 영역 URI:
|
||||
|
||||
- `/scnt/intro/statsSumryPge.do`
|
||||
- `/scnt/conts/contsStats.do`
|
||||
- `/scnt/clg/cntnStats.do`
|
||||
- `/scnt/msg/msgStats.do`
|
||||
- `/scnt/plg/pstStats.do`
|
||||
- `/scnt/srch/srchKwrdSttusStats.do`
|
||||
- `/scnt/srvy/srvyStats.do`
|
||||
|
||||
통계 범주:
|
||||
|
||||
- 콘텐츠 통계
|
||||
- 콘텐츠 접근 통계
|
||||
- 메시지 통계
|
||||
- 게시물 통계
|
||||
- 검색어 통계
|
||||
- 설문 통계
|
||||
- 요약 통계
|
||||
|
||||
운영 로그 URI 예시:
|
||||
|
||||
- `/mfuc/system/log/api/apiCommLog.do`
|
||||
- `/mfuc/system/log/cd/cmmnCdChgLog.do`
|
||||
- `/mfuc/system/log/conts/userContsAtnlcLog.do`
|
||||
- `/mfuc/system/log/err/srvrErrLog.do`
|
||||
- `/mfuc/system/log/prvc/prvcAcsDelHstryList.do`
|
||||
- `/mfuc/system/log/prvc/userInfoChgHstryList.do`
|
||||
- `/mfuc/system/log/prvc/authrtChgHstryList.do`
|
||||
|
||||
로그 범주:
|
||||
|
||||
- 웹 접근 로그
|
||||
- 로그인 관련 로그
|
||||
- 개인정보 변경/삭제 이력
|
||||
- 코드 변경 이력
|
||||
- API 사용 이력
|
||||
- 서버 에러 로그
|
||||
- 콘텐츠 접근 로그
|
||||
|
||||
## 7. 보안 및 권한 구조
|
||||
|
||||
### 7.1 인증/인가 방식
|
||||
|
||||
Spring Security + eGov Security 기반의 DB 인증 구조를 사용한다.
|
||||
|
||||
대표 설정:
|
||||
|
||||
- 로그인 URL: `/comn/user/login/loginUser.do`
|
||||
- 로그인 실패 URL: `/comn/user/login/loginUser.do?login_error=1`
|
||||
- 접근 거부 URL: `/authrt/acsReject.do`
|
||||
|
||||
보안 질의에 사용되는 대표 테이블:
|
||||
|
||||
- `TN_USER`
|
||||
- `TN_ROLE_INFO`
|
||||
- `TN_AUTHRT_ROLE_REL`
|
||||
- `TN_AUTHRT_SCLSRT`
|
||||
|
||||
코드에서 직접 확인되는 대표 권한:
|
||||
|
||||
- `ROLE_MANAGER`
|
||||
- `ROLE_TEACHER`
|
||||
- `ROLE_MONITOR`
|
||||
|
||||
이 외에도 게시판별 권한 코드는 게시판 메타데이터 기반으로 별도 적용된다.
|
||||
|
||||
### 7.2 인터셉터 기반 공통 처리
|
||||
|
||||
- `WebLogInterceptor`
|
||||
- 요청 URL, 요청자 ID, 요청자 IP 기록
|
||||
- `MenuPageInterceptor`
|
||||
- `menuSn` 기준 메뉴 트리 주입
|
||||
- `LocaleChangeInterceptor`
|
||||
- `lang` 파라미터로 locale 전환
|
||||
|
||||
### 7.3 공통 예외/검증 처리
|
||||
|
||||
- `CommonDefaultController`
|
||||
- JSON 검증 오류 조립
|
||||
- 공통 에러 로깅
|
||||
- `SiteExceptionResolver`
|
||||
- 데이터 접근 예외 및 프레임워크 예외의 에러 화면 매핑
|
||||
|
||||
## 8. 데이터 및 리소스 구조
|
||||
|
||||
### 8.1 MyBatis 매퍼 구조
|
||||
|
||||
- Mapper Interface:
|
||||
- `src/main/java/kr/iotdoor/**/mapper`
|
||||
- SQL XML:
|
||||
- `src/main/resources/egovframework/mapper/kr/iotdoor/**/*_SQL.xml`
|
||||
|
||||
Java 패키지 구조와 SQL XML 구조가 대체로 1:1에 가깝게 대응되어 있다.
|
||||
|
||||
### 8.2 파일 저장 구조
|
||||
|
||||
코드상 파일 저장은 첨부파일 저장소와 콘텐츠 저장소로 구분된다.
|
||||
|
||||
- 첨부파일 기준 프로퍼티:
|
||||
- `Globals.fileStorePath`
|
||||
- 콘텐츠 저장 기준 프로퍼티:
|
||||
- `Globals.contentsStorePath`
|
||||
- 웹 노출 루트:
|
||||
- `Globals.contentsRoot`
|
||||
|
||||
대표 저장 패턴:
|
||||
|
||||
- 게시판 첨부: `BBS/{bbsIdntfNo}/{날짜}`
|
||||
- Q&A 첨부: `QNA/{qnaIdntfNo}/{날짜}`
|
||||
- 에디터 업로드: `{구분코드}/IMAGE`, `{구분코드}/FILE`
|
||||
- 프로필/콘텐츠 이미지: 콘텐츠 루트 URL 생성 방식
|
||||
|
||||
### 8.3 외부 연동
|
||||
|
||||
Hypermeta 연동이 구현되어 있다.
|
||||
|
||||
주요 URI:
|
||||
|
||||
- `/comn/api/getContsHypermeta.json`
|
||||
- `/comn/api/getMetaHypermeta.json`
|
||||
|
||||
서비스 호출 대상:
|
||||
|
||||
- `/api/contents/search`
|
||||
- `/api/iotdoor/Lesson/list?pageIndex=...`
|
||||
|
||||
기준 URL은 `Globals.HYPERMETA_URL` 프로퍼티를 사용한다.
|
||||
|
||||
## 9. 프로젝트 규모 요약
|
||||
|
||||
현재 저장소 기준 집계 값은 다음과 같다.
|
||||
|
||||
| 항목 | 수량 |
|
||||
| --- | ---: |
|
||||
| Java 소스 파일 | 392 |
|
||||
| Controller 클래스 | 59 |
|
||||
| Service 구현 클래스 | 51 |
|
||||
| Mapper 인터페이스 | 54 |
|
||||
| MyBatis SQL XML | 53 |
|
||||
| Thymeleaf HTML 템플릿 | 156 |
|
||||
| `WEB-INF/jsp` 하위 JSP | 0 |
|
||||
| `src/test` 하위 Java 테스트 | 0 |
|
||||
|
||||
해석:
|
||||
|
||||
- 규모상 중대형 웹애플리케이션에 해당
|
||||
- 도메인 분리가 비교적 명확함
|
||||
- JSP Resolver는 남아 있으나 실제 화면은 사실상 Thymeleaf 중심
|
||||
|
||||
## 10. 운영 및 유지보수 유의사항
|
||||
|
||||
소스 분석 기준으로 아래 항목은 반드시 운영 검토가 필요하다.
|
||||
|
||||
1. Spring Security CSRF가 전역 비활성화되어 있다.
|
||||
- 위치: `src/main/resources/egovframework/spring/com/context-security.xml`
|
||||
- 현재 설정: `csrf="false"`
|
||||
- 일부 화면은 수동 세션 토큰을 쓰고 일부는 쓰지 않아 일관성이 부족하다.
|
||||
|
||||
2. 비밀번호 해시 방식이 `plaintext` 로 설정되어 있다.
|
||||
- 위치: `src/main/resources/egovframework/spring/com/context-security.xml`
|
||||
- 현재 설정: `hash="plaintext"`
|
||||
- 운영 환경 적용 여부를 재검토해야 한다.
|
||||
|
||||
3. 업로드 최대 용량 설정이 서로 다르다.
|
||||
- Multipart Resolver: `50000000` 바이트
|
||||
- 전역 프로퍼티: `1048576` 바이트
|
||||
- 실제 정책과 코드 동작을 일치시킬 필요가 있다.
|
||||
|
||||
4. 자동화 테스트 자산이 사실상 없다.
|
||||
- `src/test` 하위 Java 테스트 수: 0
|
||||
- Maven Surefire도 `skipTests=true` 로 설정되어 있다.
|
||||
|
||||
5. 저장소 내 설정 파일에 민감한 운영 값이 포함되어 있다.
|
||||
- 개발용 계정 보조값
|
||||
- 암호화 관련 값
|
||||
- 로컬 환경 연결 정보
|
||||
- 외부 보안 설정으로 분리하는 것이 바람직하다.
|
||||
|
||||
6. 스케줄링 관련 구현은 있으나 실제 활성화 여부 확인이 필요하다.
|
||||
- Quartz 의존성과 스케줄 서비스 클래스가 존재
|
||||
- 일부 XML 트리거 설정은 주석 처리되어 있다.
|
||||
|
||||
7. 페이지 편집 기능은 배포된 파일 시스템에 직접 읽기/쓰기를 수행한다.
|
||||
- 배포 계정 권한이 필요
|
||||
- 운영 배포 파이프라인과 충돌 여부를 확인해야 한다.
|
||||
|
||||
## 11. 후속 문서화 권장 항목
|
||||
|
||||
본 문서를 기반으로 아래 문서를 추가 작성하면 운영 인수인계 품질이 높아진다.
|
||||
|
||||
- DB 테이블 명세서 및 ERD
|
||||
- 화면 목록 및 메뉴 트리 문서
|
||||
- 권한 매트릭스
|
||||
- 외부 API 명세서
|
||||
- 배치/스케줄 운영 정책서
|
||||
- 배포 구조도 및 시크릿 관리 정책
|
||||
- 테스트 시나리오 및 회귀 점검 목록
|
||||
|
||||
## 12. 결론
|
||||
|
||||
IOTDOOR는 `사이트 운영 CMS + 커뮤니티 기능 + 통계/로그 백오피스` 가 결합된 통합 포털 시스템이다.
|
||||
도메인 분리와 계층 구조는 비교적 명확하며, 운영형 포털로 확장 가능한 구조를 가지고 있다.
|
||||
다만 보안 설정, 민감정보 관리, 테스트 부재, 업로드 정책 불일치, 스케줄 활성화 여부 등은 운영 안정화를 위해 우선 점검이 필요한 항목이다.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>if "Windows_NT" == "Windows_NT" setlocal
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set DIRNAME=D:\Projects\998.IOTDOOR\iotdoor_v5\
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>if "D:\Projects\998.IOTDOOR\iotdoor_v5\" == "" set DIRNAME=.
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set APP_BASE_NAME=gradlew
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set APP_HOME=D:\Projects\998.IOTDOOR\iotdoor_v5\
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>for %i in ("D:\Projects\998.IOTDOOR\iotdoor_v5\") do set APP_HOME=%~fi
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set APP_HOME=D:\Projects\998.IOTDOOR\iotdoor_v5\
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set JAVA_HOME=C:\Program Files\Java\jdk-17
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set JAVA_EXE=C:\Program Files\Java\jdk-17 /bin/java.exe
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>if exist "C:\Program Files\Java\jdk-17 /bin/java.exe" goto execute
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>echo. 1>&2
|
||||
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>echo ERROR: JAVA_HOME is set to an invalid directory: C:\Program Files\Java\jdk-17 1>&2
|
||||
ERROR: JAVA_HOME is set to an invalid directory: C:\Program Files\Java\jdk-17
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>echo. 1>&2
|
||||
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>echo location of your Java installation. 1>&2
|
||||
location of your Java installation.
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>goto fail
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>rem the _cmd.exe /c_ return code!
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>set EXIT_CODE=0
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>if 0 EQU 0 set EXIT_CODE=1
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>if not "" == "" exit 1
|
||||
|
||||
D:\Projects\998.IOTDOOR\iotdoor_v5>exit /b 1
|
||||
@@ -0,0 +1,61 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.5.11'
|
||||
}
|
||||
|
||||
group = 'kr.iotdoor'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
description = 'IOTDOOR modernized Spring Boot application'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
mybatisSpringBootVersion = '3.0.4'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
||||
runtimeOnly platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
||||
annotationProcessor platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
||||
testImplementation platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${mybatisSpringBootVersion}"
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
|
||||
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
|
||||
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:3.2.0'
|
||||
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
testRuntimeOnly 'com.h2database:h2'
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
tasks.named('bootRun') {
|
||||
// Let bootRun read templates/static resources directly from src/main/resources.
|
||||
sourceResources sourceSets.main
|
||||
systemProperty 'spring.devtools.restart.enabled', 'true'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -0,0 +1,251 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -0,0 +1,94 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,554 @@
|
||||
HTTP/1.1 200
|
||||
Set-Cookie: JSESSIONID=0D10A051D1AEAE1FBA23B6B1217704A1; Path=/; HttpOnly
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 0
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: text/html;charset=UTF-8
|
||||
Content-Language: ko-KR
|
||||
Transfer-Encoding: chunked
|
||||
Date: Wed, 15 Apr 2026 16:11:49 GMT
|
||||
Connection: close
|
||||
|
||||
2000
|
||||
<!doctype html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta name="description" content="">
|
||||
<meta property="og:title" content="">
|
||||
<meta property="og:description" content="">
|
||||
<meta property="og:url" content="">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:image" content="">
|
||||
|
||||
<title>국제오토텍</title>
|
||||
<!-- Favicon -->
|
||||
<link href="/resources/kr/iotdoor/comn/site/layout/image/template/favicon.ico" rel="icon" />
|
||||
|
||||
<!-- Google Web Fonts -->
|
||||
<link rel="preconnect" href="//fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="//fonts.gstatic.com" crossorigin />
|
||||
<link href="//fonts.googleapis.com/css2?family=Open+Sans:wght@400;500&family=Roboto:wght@500;700;900&display=swap" rel="stylesheet"/>
|
||||
|
||||
<!-- Icon Font Stylesheet -->
|
||||
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/5.10.0/css/all.min.css" rel="stylesheet"/>
|
||||
<link href="//cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css" rel="stylesheet"/>
|
||||
|
||||
<!-- Libraries Stylesheet -->
|
||||
<link href="/resources/lib/animate/animate.min.css" rel="stylesheet" />
|
||||
<link href="/resources/lib/owlcarousel/assets/owl.carousel.min.css" rel="stylesheet"/>
|
||||
<link href="/resources/lib/lightbox/css/lightbox.min.css" rel="stylesheet" />
|
||||
|
||||
<!-- Customized Bootstrap Stylesheet -->
|
||||
<link href="/resources/kr/iotdoor/comn/site/layout/css/bootstrap.min.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="/resources/kr/iotdoor/comn/site/layout/css/iotdoor.css?v=202404021718">
|
||||
|
||||
<!-- Template Stylesheet -->
|
||||
<link href="/resources/kr/iotdoor/comn/site/layout/css/templateStyle.css?220712" rel="stylesheet" />
|
||||
|
||||
<!-- <link th:if="${mainYn == 'Y' && popupList.size > 0}" rel="stylesheet" th:href="@{/resources/kr/iotdoor/comn/popup/css/popupListLayout.css}"> -->
|
||||
<!-- <script th:if="${not #strings.isEmpty(checkMessage)}">alert('');</script> -->
|
||||
<script>const global_isMber = false;</script>
|
||||
<script>
|
||||
const global_errCd = '';
|
||||
</script>
|
||||
<meta name="_csrf" content="SLzppFw0SGIOkKBdGipf2tA8MsSw_77GJf--F0dMYWmQg6PFf4nal20Ae1YjpZVleAdr6-ANH6aJmofrQM6JJCYoWVmotpKm">
|
||||
<meta name="_csrf_header" content="X-CSRF-TOKEN">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<style>
|
||||
.spinner {
|
||||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
border-left-color: #09f;
|
||||
|
||||
margin: 0 auto;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="skip-nav">
|
||||
<a href="#site-contents" class="visually-hidden">본문 바로가기</a>
|
||||
</div>
|
||||
|
||||
<!-- Spinner Start -->
|
||||
<div id="spinner" class="show bg-white position-fixed translate-middle w-100 vh-100 top-50 start-50 d-flex align-items-center justify-content-center">
|
||||
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Spinner End -->
|
||||
|
||||
|
||||
<!-- Topbar Start -->
|
||||
<div class="container-fluid bg-dark p-0">
|
||||
<div class="row gx-0 d-none d-lg-flex">
|
||||
<div class="col-lg-7 px-5 text-start">
|
||||
<div class="h-100 d-inline-flex align-items-center me-4">
|
||||
<small class="fa fa-map-marker-alt text-primary me-2"></small>
|
||||
<small>대전광역시 서구 유등로 55 102호</small>
|
||||
</div>
|
||||
<div class="h-100 d-inline-flex align-items-center">
|
||||
<small class="far fa-clock text-primary me-2"></small>
|
||||
<small>연중무휴 24시간 영업</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 px-5 text-end">
|
||||
<div class="h-100 d-inline-flex align-items-center me-4">
|
||||
<small class="fa fa-phone-alt text-primary me-2"></small>
|
||||
<small>
|
||||
<span>042-585-1749</span>
|
||||
<span> </span>
|
||||
<span>010-3476-1749</span>
|
||||
</small>
|
||||
</div>
|
||||
<div class="h-100 d-inline-flex align-items-center mx-n2">
|
||||
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i class="fab fa-facebook-f"></i></a>
|
||||
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i class="fab fa-twitter"></i></a>
|
||||
<a class="btn btn-square btn-link rounded-0 border-0 border-end border-secondary" href=""><i class="fab fa-linkedin-in"></i></a>
|
||||
<a class="btn btn-square btn-link rounded-0" href=""><i class="fab fa-instagram"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Topbar End -->
|
||||
|
||||
|
||||
<!-- Navbar Start -->
|
||||
<nav class="navbar navbar-expand-lg bg-white navbar-light sticky-top p-0">
|
||||
<a href="/" class="navbar-brand d-flex align-items-center border-end px-4 px-lg-5">
|
||||
<!-- <h2 class="m-0 text-primary">국제오토텍</h2> -->
|
||||
<img src="/resources/kr/iotdoor/comn/site/layout/image/template/logo.jpg" alt="로고" style="height:75px;">
|
||||
</a>
|
||||
<button type="button" class="navbar-toggler me-4" data-bs-toggle="collapse" data-bs-target="#navbarCollapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarCollapse">
|
||||
<div class="navbar-nav ms-auto p-4 p-lg-0">
|
||||
<a href="/" class="nav-item nav-link">Home</a>
|
||||
<a href="/" class="nav-item nav-link">About</a>
|
||||
<a href="/portfolio.do" class="nav-item nav-link">Portfolio</a>
|
||||
<a href="/contact.do" class="nav-item nav-link">Contact</a>
|
||||
<a href="/mfuc/index.do" class="nav-item nav-link">ADMIN</a>
|
||||
</div>
|
||||
<!--<a href="" class="btn btn-primary rounded-0 py-4 px-lg-5 d-none d-lg-block">Get A Quote<i class="fa fa-arrow-right ms-3"></i></a>-->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<div id="site-contents" class="content-wrap">
|
||||
|
||||
|
||||
<!-- About Start -->
|
||||
<div class="container-fluid bg-light overflow-hidden my-5 px-lg-0">
|
||||
<div class="container about px-lg-0">
|
||||
<div class="row g-0 mx-lg-0">
|
||||
<div
|
||||
class="col-lg-6 ps-lg-0 wow fadeIn"
|
||||
data-wow-delay="0.1s"
|
||||
style="min-height: 400px"
|
||||
>
|
||||
<div class="position-relative h-100">
|
||||
<img
|
||||
class="position-absolute img-fluid w-100 h-100"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/about.jpg"
|
||||
style="object-fit: cover"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-lg-6 about-text py-5 wow fadeIn"
|
||||
data-wow-delay="0.5s"
|
||||
>
|
||||
<div class="p-lg-5 pe-lg-0">
|
||||
<h6 class="text-primary">About Us</h6>
|
||||
<h1 class="mb-4">
|
||||
국제오토텍자동문
|
||||
</h1>
|
||||
<p>
|
||||
OVER-HEAD DOOR, AUTO GATE
|
||||
</p>
|
||||
<p>
|
||||
SLIDING DOOR, PARKING DOOR, ROLLER SHUTTER
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<i class="fa fa-check-circle text-primary me-3"></i>
|
||||
슬라이딩자동문
|
||||
</p>
|
||||
<p>
|
||||
<i class="fa fa-check-circle text-primary me-3"></i>
|
||||
산업용자동문(고속스피드도어)
|
||||
</p>
|
||||
<p>
|
||||
<i class="fa fa-check-circle text-primary me-3"></i>
|
||||
내풍압셔터(알
|
||||
2000
|
||||
루미늄셔터)
|
||||
</p>
|
||||
<p>
|
||||
<i class="fa fa-check-circle text-primary me-3"></i>
|
||||
오버헤드도어
|
||||
</p>
|
||||
<!--<a
|
||||
href=""
|
||||
class="btn btn-primary rounded-pill py-3 px-5 mt-3"
|
||||
>Explore More</a
|
||||
>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- About End -->
|
||||
|
||||
<!-- Service Start -->
|
||||
<div class="container-xxl py-5">
|
||||
<div class="container">
|
||||
<div
|
||||
class="text-center mx-auto mb-5 wow fadeInUp"
|
||||
data-wow-delay="0.1s"
|
||||
style="max-width: 600px"
|
||||
>
|
||||
<h6 class="text-primary">Our Services</h6>
|
||||
<h1 class="mb-4">
|
||||
고객님이 원하시는 서비스는<br/>무엇이든 가능합니다
|
||||
</h1>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
<div
|
||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
||||
data-wow-delay="0.1s"
|
||||
>
|
||||
<div class="service-item rounded overflow-hidden">
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-1.jpg"
|
||||
alt=""
|
||||
/>
|
||||
<div class="position-relative p-4 pt-0">
|
||||
<div class="service-icon">
|
||||
<i class="fa fa-solar-panel fa-3x"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">슬라이딩자동문</h4>
|
||||
<!-- <p>슬라이드 오토도어를 전문적으로 다룹니다.</p> -->
|
||||
<a class="small fw-medium" href="/portfolio.do"
|
||||
>Read More<i class="fa fa-arrow-right ms-2"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
||||
data-wow-delay="0.3s"
|
||||
>
|
||||
<div class="service-item rounded overflow-hidden">
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-2.jpg"
|
||||
alt=""
|
||||
/>
|
||||
<div class="position-relative p-4 pt-0">
|
||||
<div class="service-icon">
|
||||
<i class="fa fa-wind fa-3x"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">산업용자동문</h4>
|
||||
<a class="small fw-medium" href="/portfolio.do"
|
||||
>Read More<i
|
||||
class="fa fa-arrow-right ms-2"
|
||||
></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
||||
data-wow-delay="0.5s"
|
||||
>
|
||||
<div class="service-item rounded overflow-hidden">
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-3.jpg"
|
||||
alt=""
|
||||
/>
|
||||
<div class="position-relative p-4 pt-0">
|
||||
<div class="service-icon">
|
||||
<i class="fa fa-lightbulb fa-3x"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">고급산업용자동문</h4>
|
||||
<a class="small fw-medium" href="/portfolio.do"
|
||||
>Read More<i
|
||||
class="fa fa-arrow-right ms-2"
|
||||
></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
||||
data-wow-delay="0.1s"
|
||||
>
|
||||
<div class="service-item rounded overflow-hidden">
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-4.jpg"
|
||||
alt=""
|
||||
/>
|
||||
<div class="position-relative p-4 pt-0">
|
||||
<div class="service-icon">
|
||||
<i class="fa fa-solar-panel fa-3x"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">내풍압고속셔터</h4>
|
||||
<a class="small fw-medium" href="/portfolio.do"
|
||||
>Read More<i
|
||||
class="fa fa-arrow-right ms-2"
|
||||
></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
||||
data-wow-delay="0.3s"
|
||||
>
|
||||
<div class="service-item rounded overflow-hidden">
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-5.jpg?1"
|
||||
alt=""
|
||||
/>
|
||||
<div class="position-relative p-4 pt-0">
|
||||
<div class="service-icon">
|
||||
<i class="fa fa-wind fa-3x"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">오버헤드도어</h4>
|
||||
<a class="small fw-medium" href="/portfolio.do"
|
||||
>Read More<i
|
||||
class="fa fa-arrow-right ms-2"
|
||||
></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-md-6 col-lg-4 wow fadeInUp"
|
||||
data-wow-delay="0.5s"
|
||||
>
|
||||
<div class="service-item rounded overflow-hidden">
|
||||
<img
|
||||
class="img-fluid"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/img-600x400-6.jpg?1?1"
|
||||
alt=""
|
||||
/>
|
||||
<div class="position-relative p-4 pt-0">
|
||||
<div class="service-icon">
|
||||
<i class="fa fa-lightbulb fa-3x"></i>
|
||||
</div>
|
||||
<h4 class="mb-3">방화용슬라이딩자동문</h4>
|
||||
<a class="small fw-medium" href="/portfolio.do"
|
||||
>Read More<i
|
||||
class="fa fa-arrow-right ms-2"
|
||||
></i
|
||||
></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Service End -->
|
||||
|
||||
<!-- Feature Start -->
|
||||
<div class="container-fluid bg-light overflow-hidden my-5 px-lg-0">
|
||||
<div class="container feature px-lg-0">
|
||||
<div class="row g-0 mx-lg-0">
|
||||
|
||||
2000
|
||||
<div
|
||||
class="col-lg-6 feature-text py-5 wow fadeIn"
|
||||
data-wow-delay="0.1s"
|
||||
>
|
||||
<div class="p-lg-5 ps-lg-0">
|
||||
<h6 class="text-primary">Why Choose Us!</h6>
|
||||
<h1 class="mb-4">
|
||||
고객여러분의 가치있는 선택에 보답하겠습니다.
|
||||
</h1>
|
||||
<p class="mb-4 pb-2">
|
||||
자동문 전문 업체로 철거.시공.A/S까지 원스톱으로 상담해드립니다.
|
||||
<br>
|
||||
전문기계교체상담.오퍼레이터교체.판매문의 환영합니다.
|
||||
</p>
|
||||
<div class="row g-4">
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="btn-lg-square bg-primary rounded-circle"
|
||||
>
|
||||
<i
|
||||
class="fa fa-check text-white"
|
||||
></i>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<p class="mb-0">Quality</p>
|
||||
<h5 class="mb-0">Services</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="btn-lg-square bg-primary rounded-circle"
|
||||
>
|
||||
<i
|
||||
class="fa fa-user-check text-white"
|
||||
></i>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<p class="mb-0">Expert</p>
|
||||
<h5 class="mb-0">Workers</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="btn-lg-square bg-primary rounded-circle"
|
||||
>
|
||||
<i
|
||||
class="fa fa-drafting-compass text-white"
|
||||
></i>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<p class="mb-0">Free</p>
|
||||
<h5 class="mb-0">Consultation</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex align-items-center">
|
||||
<div
|
||||
class="btn-lg-square bg-primary rounded-circle"
|
||||
>
|
||||
<i
|
||||
class="fa fa-headphones text-white"
|
||||
></i>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<p class="mb-0">Customer</p>
|
||||
<h5 class="mb-0">Support</h5>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-lg-6 pe-lg-0 wow fadeIn"
|
||||
data-wow-delay="0.5s"
|
||||
style="min-height: 400px"
|
||||
>
|
||||
<div class="position-relative h-100">
|
||||
<img
|
||||
class="position-absolute img-fluid w-100 h-100"
|
||||
src="/resources/kr/iotdoor/comn/site/layout/image/template/feature.jpg"
|
||||
style="object-fit: cover"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Feature End -->
|
||||
|
||||
<!-- Contact Start -->
|
||||
<div class="container-fluid bg-light overflow-hidden px-lg-0" style="margin: 6rem 0;">
|
||||
<div class="container contact px-lg-0">
|
||||
<div class="row g-0 mx-lg-0">
|
||||
<div class="col-lg-6 p-lg-5 " style="min-height: 300px;">
|
||||
<div class="position-relative h-100">
|
||||
<div id="map" style="width:100%;height:500px;"></div>
|
||||
<code id="snippet" class="snippet"></code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 contact-text wow fadeIn" data-wow-delay="0.5s">
|
||||
<div class="p-lg-5 ps-lg-0">
|
||||
<h6 class="text-primary">Contact Us</h6>
|
||||
<p class="mb-4">내용을 자세하게 입력해주시면 상담진행시에 좀 더 원활하게 상담이 가능합니다. 연중무휴 24시간 주저말고 남겨주세요. </p>
|
||||
<form>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="name" placeholder="Your Name">
|
||||
<label for="name">Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-floating">
|
||||
<input type="email" class="form-control" id="email" placeholder="Your Email" >
|
||||
<label for="email">Email</label>
|
||||
</div>
|
||||
<div id="emailError" style="color:red; font-size:13px; display:none;">
|
||||
올바른 이메일 형식이 아닙니다.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="phone" placeholder="Phone" >
|
||||
<label for="subject">Phone</label>
|
||||
</div>
|
||||
<div id="phoneError" style="color:red; font-size:13px; display:none;">
|
||||
올바른 휴대폰 형식이 아닙니다.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
<input type="text" class="form-control" id="subject" placeholder="Subject">
|
||||
<label for="subject">Subject</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-floating">
|
||||
|
||||
34e
|
||||
<textarea class="form-control" placeholder="Leave a message here" id="message" style="height: 100px"></textarea>
|
||||
<label for="message">Message</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button id="btnSendMail" class="btn btn-primary rounded-pill py-3 px-5" type="button">Send Message</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Contact End -->
|
||||
|
||||
<!-- <?php include_once './common/footer.php'?> -->
|
||||
|
||||
<!-- Template Javascript -->
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
rootProject.name = 'iotdoor-boot'
|
||||
@@ -0,0 +1,14 @@
|
||||
package kr.iotdoor.boot;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class IotdoorBootApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(IotdoorBootApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package kr.iotdoor.boot.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "iotdoor")
|
||||
public class IotdoorProperties {
|
||||
|
||||
private final Site site = new Site();
|
||||
private final Security security = new Security();
|
||||
private final File file = new File();
|
||||
|
||||
public Site getSite() {
|
||||
return site;
|
||||
}
|
||||
|
||||
public Security getSecurity() {
|
||||
return security;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public static class Site {
|
||||
private String name;
|
||||
private String adminTitle;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String phoneAlt;
|
||||
private String address;
|
||||
private String summary;
|
||||
private String description;
|
||||
private double mapLatitude;
|
||||
private double mapLongitude;
|
||||
private String mapClientId;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAdminTitle() {
|
||||
return adminTitle;
|
||||
}
|
||||
|
||||
public void setAdminTitle(String adminTitle) {
|
||||
this.adminTitle = adminTitle;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getPhoneAlt() {
|
||||
return phoneAlt;
|
||||
}
|
||||
|
||||
public void setPhoneAlt(String phoneAlt) {
|
||||
this.phoneAlt = phoneAlt;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public double getMapLatitude() {
|
||||
return mapLatitude;
|
||||
}
|
||||
|
||||
public void setMapLatitude(double mapLatitude) {
|
||||
this.mapLatitude = mapLatitude;
|
||||
}
|
||||
|
||||
public double getMapLongitude() {
|
||||
return mapLongitude;
|
||||
}
|
||||
|
||||
public void setMapLongitude(double mapLongitude) {
|
||||
this.mapLongitude = mapLongitude;
|
||||
}
|
||||
|
||||
public String getMapClientId() {
|
||||
return mapClientId;
|
||||
}
|
||||
|
||||
public void setMapClientId(String mapClientId) {
|
||||
this.mapClientId = mapClientId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Security {
|
||||
private String adminUsername;
|
||||
private String adminPassword;
|
||||
private String adminDisplayName;
|
||||
|
||||
public String getAdminUsername() {
|
||||
return adminUsername;
|
||||
}
|
||||
|
||||
public void setAdminUsername(String adminUsername) {
|
||||
this.adminUsername = adminUsername;
|
||||
}
|
||||
|
||||
public String getAdminPassword() {
|
||||
return adminPassword;
|
||||
}
|
||||
|
||||
public void setAdminPassword(String adminPassword) {
|
||||
this.adminPassword = adminPassword;
|
||||
}
|
||||
|
||||
public String getAdminDisplayName() {
|
||||
return adminDisplayName;
|
||||
}
|
||||
|
||||
public void setAdminDisplayName(String adminDisplayName) {
|
||||
this.adminDisplayName = adminDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class File {
|
||||
private String uploadPath;
|
||||
|
||||
public String getUploadPath() {
|
||||
return uploadPath;
|
||||
}
|
||||
|
||||
public void setUploadPath(String uploadPath) {
|
||||
this.uploadPath = uploadPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package kr.iotdoor.boot.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers(
|
||||
"/",
|
||||
"/index.do",
|
||||
"/portfolio.do",
|
||||
"/portfolio/**",
|
||||
"/contact.do",
|
||||
"/api/contact",
|
||||
"/comn/file/view/**",
|
||||
"/comn/user/login/login.do",
|
||||
"/comn/user/login/loginUser.do",
|
||||
"/resources/**",
|
||||
"/error",
|
||||
"/actuator/health"
|
||||
).permitAll()
|
||||
.requestMatchers("/mfuc/**", "/mngr/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/comn/user/login/loginUser.do")
|
||||
.loginProcessingUrl("/comn/user/login/actionLogin.do")
|
||||
.usernameParameter("id")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/mfuc/index.do", true)
|
||||
.failureUrl("/comn/user/login/loginUser.do?login_error=1")
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/comn/user/login/logout.do")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID")
|
||||
)
|
||||
.rememberMe(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.file.StoredFile;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AtchFileDetailMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_ATCH_FILE_DTL (
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
FILE_SN,
|
||||
FILE_STRG_PATH_NM,
|
||||
FILE_STRG_NM,
|
||||
ORGNL_FILE_NM,
|
||||
FILE_EXTN_NM,
|
||||
FILE_CN,
|
||||
FILE_SZ
|
||||
) VALUES (
|
||||
#{atchFileIdntfNo},
|
||||
#{fileSn},
|
||||
#{fileStrgPathNm},
|
||||
#{fileStrgNm},
|
||||
#{orgnlFileNm},
|
||||
#{fileExtnNm},
|
||||
#{fileCn},
|
||||
#{fileSz}
|
||||
)
|
||||
""")
|
||||
int insertAtchFileDtl(StoredFile storedFile);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(MAX(FILE_SN), 0) + 1 AS FILE_SN
|
||||
FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
""")
|
||||
int selectAtchFileDtlNextSn(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
FILE_SN,
|
||||
FILE_STRG_PATH_NM,
|
||||
FILE_STRG_NM,
|
||||
ORGNL_FILE_NM,
|
||||
FILE_EXTN_NM,
|
||||
FILE_CN,
|
||||
COALESCE(FILE_SZ, 0) AS FILE_SZ
|
||||
FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
""")
|
||||
List<StoredFile> selectAtchFileDtlList(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
FILE_SN,
|
||||
FILE_STRG_PATH_NM,
|
||||
FILE_STRG_NM,
|
||||
ORGNL_FILE_NM,
|
||||
FILE_EXTN_NM,
|
||||
FILE_CN,
|
||||
COALESCE(FILE_SZ, 0) AS FILE_SZ
|
||||
FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
AND FILE_SN = #{fileSn}
|
||||
""")
|
||||
StoredFile selectAtchFileDtl(@Param("atchFileIdntfNo") String atchFileIdntfNo, @Param("fileSn") int fileSn);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
AND FILE_SN = #{fileSn}
|
||||
""")
|
||||
int deleteAtchFileDtl(@Param("atchFileIdntfNo") String atchFileIdntfNo, @Param("fileSn") int fileSn);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface AtchFileMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_ATCH_FILE (
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
CRT_DT,
|
||||
DEL_YN
|
||||
) VALUES (
|
||||
#{atchFileIdntfNo},
|
||||
CURRENT_TIMESTAMP,
|
||||
'N'
|
||||
)
|
||||
""")
|
||||
int insertAtchFile(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_ATCH_FILE SET
|
||||
DEL_YN = 'Y'
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
""")
|
||||
int deleteAtchFile(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.contact.ContactSubmission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ContactMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_CONTACT_INQRY (
|
||||
CONTACT_IDNTF_NO,
|
||||
NM,
|
||||
EML_ADDR,
|
||||
TELNO,
|
||||
TTL,
|
||||
CN,
|
||||
SUBMIT_DT
|
||||
) VALUES (
|
||||
#{contactIdntfNo},
|
||||
#{name},
|
||||
#{email},
|
||||
#{phone},
|
||||
#{subject},
|
||||
#{message},
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
int insertContact(ContactSubmission contactSubmission);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
CONTACT_IDNTF_NO,
|
||||
NM AS NAME,
|
||||
EML_ADDR AS EMAIL,
|
||||
TELNO AS PHONE,
|
||||
TTL AS SUBJECT,
|
||||
CN AS MESSAGE,
|
||||
SUBMIT_DT AS SUBMITTED_AT
|
||||
FROM TN_CONTACT_INQRY
|
||||
ORDER BY SUBMIT_DT DESC
|
||||
LIMIT #{limit}
|
||||
""")
|
||||
List<ContactSubmission> selectRecentContacts(@Param("limit") int limit);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
CONTACT_IDNTF_NO,
|
||||
NM AS NAME,
|
||||
EML_ADDR AS EMAIL,
|
||||
TELNO AS PHONE,
|
||||
TTL AS SUBJECT,
|
||||
CN AS MESSAGE,
|
||||
SUBMIT_DT AS SUBMITTED_AT
|
||||
FROM TN_CONTACT_INQRY
|
||||
ORDER BY SUBMIT_DT DESC
|
||||
""")
|
||||
List<ContactSubmission> selectAllContacts();
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM TN_CONTACT_INQRY
|
||||
""")
|
||||
int selectContactCount();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioItem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PortfolioMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
p.PORTFOLIO_IDNTF_NO,
|
||||
p.TITLE,
|
||||
p.CONTENT,
|
||||
p.THUMBNAIL_ATCH_FILE_IDNTF_NO,
|
||||
p.THUMBNAIL_FILE_SN,
|
||||
p.REG_DT AS REGISTERED_AT,
|
||||
p.INQ_CNT AS VIEW_COUNT,
|
||||
p.REG_USER_IDNTF_NO AS REGISTERED_BY_USER_IDNTF_NO,
|
||||
u.USER_NM AS REGISTERED_BY_NAME,
|
||||
p.DEL_YN,
|
||||
p.UPD_DT AS UPDATED_AT
|
||||
FROM TN_PORTFOLIO p
|
||||
JOIN TN_USER u ON u.USER_IDNTF_NO = p.REG_USER_IDNTF_NO
|
||||
WHERE p.DEL_YN = 'N'
|
||||
ORDER BY p.REG_DT DESC
|
||||
""")
|
||||
List<PortfolioItem> selectPortfolioList();
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
p.PORTFOLIO_IDNTF_NO,
|
||||
p.TITLE,
|
||||
p.CONTENT,
|
||||
p.THUMBNAIL_ATCH_FILE_IDNTF_NO,
|
||||
p.THUMBNAIL_FILE_SN,
|
||||
p.REG_DT AS REGISTERED_AT,
|
||||
p.INQ_CNT AS VIEW_COUNT,
|
||||
p.REG_USER_IDNTF_NO AS REGISTERED_BY_USER_IDNTF_NO,
|
||||
u.USER_NM AS REGISTERED_BY_NAME,
|
||||
p.DEL_YN,
|
||||
p.UPD_DT AS UPDATED_AT
|
||||
FROM TN_PORTFOLIO p
|
||||
JOIN TN_USER u ON u.USER_IDNTF_NO = p.REG_USER_IDNTF_NO
|
||||
WHERE p.PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
AND p.DEL_YN = 'N'
|
||||
""")
|
||||
PortfolioItem selectPortfolioDetail(@Param("portfolioIdntfNo") String portfolioIdntfNo);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_PORTFOLIO (
|
||||
PORTFOLIO_IDNTF_NO,
|
||||
TITLE,
|
||||
THUMBNAIL_ATCH_FILE_IDNTF_NO,
|
||||
THUMBNAIL_FILE_SN,
|
||||
CONTENT,
|
||||
REG_DT,
|
||||
INQ_CNT,
|
||||
REG_USER_IDNTF_NO,
|
||||
DEL_YN,
|
||||
UPD_DT
|
||||
) VALUES (
|
||||
#{portfolioIdntfNo},
|
||||
#{title},
|
||||
#{thumbnailAtchFileIdntfNo},
|
||||
#{thumbnailFileSn},
|
||||
#{content},
|
||||
#{registeredAt},
|
||||
0,
|
||||
#{registeredByUserIdntfNo},
|
||||
'N',
|
||||
NULL
|
||||
)
|
||||
""")
|
||||
int insertPortfolio(PortfolioItem portfolioItem);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_PORTFOLIO
|
||||
SET
|
||||
TITLE = #{title},
|
||||
THUMBNAIL_ATCH_FILE_IDNTF_NO = #{thumbnailAtchFileIdntfNo},
|
||||
THUMBNAIL_FILE_SN = #{thumbnailFileSn},
|
||||
CONTENT = #{content},
|
||||
UPD_DT = #{updatedAt}
|
||||
WHERE PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
AND DEL_YN = 'N'
|
||||
""")
|
||||
int updatePortfolio(PortfolioItem portfolioItem);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_PORTFOLIO
|
||||
SET
|
||||
DEL_YN = 'Y',
|
||||
UPD_DT = CURRENT_TIMESTAMP
|
||||
WHERE PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
""")
|
||||
int deletePortfolio(@Param("portfolioIdntfNo") String portfolioIdntfNo);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_PORTFOLIO
|
||||
SET INQ_CNT = INQ_CNT + 1
|
||||
WHERE PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
AND DEL_YN = 'N'
|
||||
""")
|
||||
int incrementViewCount(@Param("portfolioIdntfNo") String portfolioIdntfNo);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM TN_PORTFOLIO
|
||||
WHERE DEL_YN = 'N'
|
||||
""")
|
||||
int countPortfolio();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.user.UserAccount;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
USER_IDNTF_NO,
|
||||
USER_ID,
|
||||
PSWD,
|
||||
USER_NM,
|
||||
EML_ADDR,
|
||||
USER_STTS_CD,
|
||||
USER_TYPE_CD,
|
||||
JOIN_DT,
|
||||
INDVDL_INFO_AGRE_DT,
|
||||
LOCK_YN,
|
||||
LGN_FAIL_NMTM,
|
||||
LOCK_DT,
|
||||
LAST_LGN_DT
|
||||
FROM TN_USER
|
||||
WHERE USER_ID = #{userId}
|
||||
""")
|
||||
UserAccount selectByUserId(@Param("userId") String userId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM TN_USER
|
||||
WHERE USER_ID = #{userId}
|
||||
""")
|
||||
int countByUserId(@Param("userId") String userId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_USER (
|
||||
USER_IDNTF_NO,
|
||||
USER_ID,
|
||||
PSWD,
|
||||
USER_NM,
|
||||
EML_ADDR,
|
||||
USER_STTS_CD,
|
||||
USER_TYPE_CD,
|
||||
JOIN_DT,
|
||||
INDVDL_INFO_AGRE_DT,
|
||||
LOCK_YN,
|
||||
LGN_FAIL_NMTM,
|
||||
LOCK_DT,
|
||||
LAST_LGN_DT
|
||||
) VALUES (
|
||||
#{userIdntfNo},
|
||||
#{userId},
|
||||
#{pswd},
|
||||
#{userNm},
|
||||
#{emlAddr},
|
||||
#{userSttsCd},
|
||||
#{userTypeCd},
|
||||
#{joinDt},
|
||||
#{indvdlInfoAgreDt},
|
||||
#{lockYn},
|
||||
#{lgnFailNmtm},
|
||||
#{lockDt},
|
||||
#{lastLgnDt}
|
||||
)
|
||||
""")
|
||||
int insertUser(UserAccount userAccount);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_USER
|
||||
SET LAST_LGN_DT = CURRENT_TIMESTAMP
|
||||
WHERE USER_IDNTF_NO = #{userIdntfNo}
|
||||
""")
|
||||
int updateLastLoginDt(@Param("userIdntfNo") String userIdntfNo);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package kr.iotdoor.boot.model.admin;
|
||||
|
||||
public record MigrationFeature(
|
||||
String name,
|
||||
String status,
|
||||
String summary,
|
||||
String routes
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.iotdoor.boot.model.contact;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record ContactRequest(
|
||||
@NotBlank(message = "이름은 필수입니다.")
|
||||
@Size(max = 60, message = "이름은 60자 이하여야 합니다.")
|
||||
String name,
|
||||
|
||||
@NotBlank(message = "이메일은 필수입니다.")
|
||||
@Email(message = "올바른 이메일 형식이 아닙니다.")
|
||||
@Size(max = 120, message = "이메일은 120자 이하여야 합니다.")
|
||||
String email,
|
||||
|
||||
@NotBlank(message = "연락처는 필수입니다.")
|
||||
@Pattern(regexp = "^01[016789]-?\\d{3,4}-?\\d{4}$", message = "올바른 휴대폰번호 형식이 아닙니다.")
|
||||
String phone,
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다.")
|
||||
@Size(max = 120, message = "제목은 120자 이하여야 합니다.")
|
||||
String subject,
|
||||
|
||||
@NotBlank(message = "문의 내용은 필수입니다.")
|
||||
@Size(max = 3000, message = "문의 내용은 3000자 이하여야 합니다.")
|
||||
String message
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package kr.iotdoor.boot.model.contact;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class ContactSubmission {
|
||||
|
||||
private String contactIdntfNo;
|
||||
private String name;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String subject;
|
||||
private String message;
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
public String getContactIdntfNo() {
|
||||
return contactIdntfNo;
|
||||
}
|
||||
|
||||
public void setContactIdntfNo(String contactIdntfNo) {
|
||||
this.contactIdntfNo = contactIdntfNo;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public LocalDateTime getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
public void setSubmittedAt(LocalDateTime submittedAt) {
|
||||
this.submittedAt = submittedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package kr.iotdoor.boot.model.file;
|
||||
|
||||
public class StoredFile {
|
||||
|
||||
private String atchFileIdntfNo;
|
||||
private int fileSn;
|
||||
private String fileStrgPathNm;
|
||||
private String fileStrgNm;
|
||||
private String orgnlFileNm;
|
||||
private String fileExtnNm;
|
||||
private String fileCn;
|
||||
private long fileSz;
|
||||
private String url;
|
||||
|
||||
public String getAtchFileIdntfNo() {
|
||||
return atchFileIdntfNo;
|
||||
}
|
||||
|
||||
public void setAtchFileIdntfNo(String atchFileIdntfNo) {
|
||||
this.atchFileIdntfNo = atchFileIdntfNo;
|
||||
}
|
||||
|
||||
public int getFileSn() {
|
||||
return fileSn;
|
||||
}
|
||||
|
||||
public void setFileSn(int fileSn) {
|
||||
this.fileSn = fileSn;
|
||||
}
|
||||
|
||||
public String getFileStrgPathNm() {
|
||||
return fileStrgPathNm;
|
||||
}
|
||||
|
||||
public void setFileStrgPathNm(String fileStrgPathNm) {
|
||||
this.fileStrgPathNm = fileStrgPathNm;
|
||||
}
|
||||
|
||||
public String getFileStrgNm() {
|
||||
return fileStrgNm;
|
||||
}
|
||||
|
||||
public void setFileStrgNm(String fileStrgNm) {
|
||||
this.fileStrgNm = fileStrgNm;
|
||||
}
|
||||
|
||||
public String getOrgnlFileNm() {
|
||||
return orgnlFileNm;
|
||||
}
|
||||
|
||||
public void setOrgnlFileNm(String orgnlFileNm) {
|
||||
this.orgnlFileNm = orgnlFileNm;
|
||||
}
|
||||
|
||||
public String getFileExtnNm() {
|
||||
return fileExtnNm;
|
||||
}
|
||||
|
||||
public void setFileExtnNm(String fileExtnNm) {
|
||||
this.fileExtnNm = fileExtnNm;
|
||||
}
|
||||
|
||||
public String getFileCn() {
|
||||
return fileCn;
|
||||
}
|
||||
|
||||
public void setFileCn(String fileCn) {
|
||||
this.fileCn = fileCn;
|
||||
}
|
||||
|
||||
public long getFileSz() {
|
||||
return fileSz;
|
||||
}
|
||||
|
||||
public void setFileSz(long fileSz) {
|
||||
this.fileSz = fileSz;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package kr.iotdoor.boot.model.portfolio;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class PortfolioForm {
|
||||
|
||||
private String portfolioIdntfNo;
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다.")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "내용은 필수입니다.")
|
||||
private String content;
|
||||
|
||||
private String thumbnailAtchFileIdntfNo;
|
||||
|
||||
public String getPortfolioIdntfNo() {
|
||||
return portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public void setPortfolioIdntfNo(String portfolioIdntfNo) {
|
||||
this.portfolioIdntfNo = portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getThumbnailAtchFileIdntfNo() {
|
||||
return thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
|
||||
public void setThumbnailAtchFileIdntfNo(String thumbnailAtchFileIdntfNo) {
|
||||
this.thumbnailAtchFileIdntfNo = thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package kr.iotdoor.boot.model.portfolio;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class PortfolioItem {
|
||||
|
||||
private String portfolioIdntfNo;
|
||||
private String title;
|
||||
private String summary;
|
||||
private String imageUrl;
|
||||
private String content;
|
||||
private String thumbnailAtchFileIdntfNo;
|
||||
private int thumbnailFileSn;
|
||||
private LocalDateTime registeredAt;
|
||||
private long viewCount;
|
||||
private String registeredByUserIdntfNo;
|
||||
private String registeredByName;
|
||||
private String delYn;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public String getPortfolioIdntfNo() {
|
||||
return portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public void setPortfolioIdntfNo(String portfolioIdntfNo) {
|
||||
this.portfolioIdntfNo = portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getThumbnailAtchFileIdntfNo() {
|
||||
return thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
|
||||
public void setThumbnailAtchFileIdntfNo(String thumbnailAtchFileIdntfNo) {
|
||||
this.thumbnailAtchFileIdntfNo = thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
|
||||
public int getThumbnailFileSn() {
|
||||
return thumbnailFileSn;
|
||||
}
|
||||
|
||||
public void setThumbnailFileSn(int thumbnailFileSn) {
|
||||
this.thumbnailFileSn = thumbnailFileSn;
|
||||
}
|
||||
|
||||
public LocalDateTime getRegisteredAt() {
|
||||
return registeredAt;
|
||||
}
|
||||
|
||||
public void setRegisteredAt(LocalDateTime registeredAt) {
|
||||
this.registeredAt = registeredAt;
|
||||
}
|
||||
|
||||
public long getViewCount() {
|
||||
return viewCount;
|
||||
}
|
||||
|
||||
public void setViewCount(long viewCount) {
|
||||
this.viewCount = viewCount;
|
||||
}
|
||||
|
||||
public String getRegisteredByUserIdntfNo() {
|
||||
return registeredByUserIdntfNo;
|
||||
}
|
||||
|
||||
public void setRegisteredByUserIdntfNo(String registeredByUserIdntfNo) {
|
||||
this.registeredByUserIdntfNo = registeredByUserIdntfNo;
|
||||
}
|
||||
|
||||
public String getRegisteredByName() {
|
||||
return registeredByName;
|
||||
}
|
||||
|
||||
public void setRegisteredByName(String registeredByName) {
|
||||
this.registeredByName = registeredByName;
|
||||
}
|
||||
|
||||
public String getDelYn() {
|
||||
return delYn;
|
||||
}
|
||||
|
||||
public void setDelYn(String delYn) {
|
||||
this.delYn = delYn;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package kr.iotdoor.boot.model.user;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class AdminUserPrincipal implements UserDetails {
|
||||
|
||||
private final UserAccount userAccount;
|
||||
|
||||
public AdminUserPrincipal(UserAccount userAccount) {
|
||||
this.userAccount = userAccount;
|
||||
}
|
||||
|
||||
public String getUserIdntfNo() {
|
||||
return userAccount.getUserIdntfNo();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return userAccount.getUserNm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return userAccount.getPswd();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return userAccount.getUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return !"Y".equalsIgnoreCase(userAccount.getLockYn());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "A".equalsIgnoreCase(userAccount.getUserSttsCd()) || "P".equalsIgnoreCase(userAccount.getUserSttsCd());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package kr.iotdoor.boot.model.user;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class UserAccount {
|
||||
|
||||
private String userIdntfNo;
|
||||
private String userId;
|
||||
private String pswd;
|
||||
private String userNm;
|
||||
private String emlAddr;
|
||||
private String userSttsCd;
|
||||
private String userTypeCd;
|
||||
private LocalDateTime joinDt;
|
||||
private LocalDateTime indvdlInfoAgreDt;
|
||||
private String lockYn;
|
||||
private Integer lgnFailNmtm;
|
||||
private LocalDateTime lockDt;
|
||||
private LocalDateTime lastLgnDt;
|
||||
|
||||
public String getUserIdntfNo() {
|
||||
return userIdntfNo;
|
||||
}
|
||||
|
||||
public void setUserIdntfNo(String userIdntfNo) {
|
||||
this.userIdntfNo = userIdntfNo;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getPswd() {
|
||||
return pswd;
|
||||
}
|
||||
|
||||
public void setPswd(String pswd) {
|
||||
this.pswd = pswd;
|
||||
}
|
||||
|
||||
public String getUserNm() {
|
||||
return userNm;
|
||||
}
|
||||
|
||||
public void setUserNm(String userNm) {
|
||||
this.userNm = userNm;
|
||||
}
|
||||
|
||||
public String getEmlAddr() {
|
||||
return emlAddr;
|
||||
}
|
||||
|
||||
public void setEmlAddr(String emlAddr) {
|
||||
this.emlAddr = emlAddr;
|
||||
}
|
||||
|
||||
public String getUserSttsCd() {
|
||||
return userSttsCd;
|
||||
}
|
||||
|
||||
public void setUserSttsCd(String userSttsCd) {
|
||||
this.userSttsCd = userSttsCd;
|
||||
}
|
||||
|
||||
public String getUserTypeCd() {
|
||||
return userTypeCd;
|
||||
}
|
||||
|
||||
public void setUserTypeCd(String userTypeCd) {
|
||||
this.userTypeCd = userTypeCd;
|
||||
}
|
||||
|
||||
public LocalDateTime getJoinDt() {
|
||||
return joinDt;
|
||||
}
|
||||
|
||||
public void setJoinDt(LocalDateTime joinDt) {
|
||||
this.joinDt = joinDt;
|
||||
}
|
||||
|
||||
public LocalDateTime getIndvdlInfoAgreDt() {
|
||||
return indvdlInfoAgreDt;
|
||||
}
|
||||
|
||||
public void setIndvdlInfoAgreDt(LocalDateTime indvdlInfoAgreDt) {
|
||||
this.indvdlInfoAgreDt = indvdlInfoAgreDt;
|
||||
}
|
||||
|
||||
public String getLockYn() {
|
||||
return lockYn;
|
||||
}
|
||||
|
||||
public void setLockYn(String lockYn) {
|
||||
this.lockYn = lockYn;
|
||||
}
|
||||
|
||||
public Integer getLgnFailNmtm() {
|
||||
return lgnFailNmtm;
|
||||
}
|
||||
|
||||
public void setLgnFailNmtm(Integer lgnFailNmtm) {
|
||||
this.lgnFailNmtm = lgnFailNmtm;
|
||||
}
|
||||
|
||||
public LocalDateTime getLockDt() {
|
||||
return lockDt;
|
||||
}
|
||||
|
||||
public void setLockDt(LocalDateTime lockDt) {
|
||||
this.lockDt = lockDt;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastLgnDt() {
|
||||
return lastLgnDt;
|
||||
}
|
||||
|
||||
public void setLastLgnDt(LocalDateTime lastLgnDt) {
|
||||
this.lastLgnDt = lastLgnDt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.config.IotdoorProperties;
|
||||
import kr.iotdoor.boot.mapper.UserMapper;
|
||||
import kr.iotdoor.boot.model.user.UserAccount;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Configuration
|
||||
public class AdminAccountInitializer {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AdminAccountInitializer.class);
|
||||
|
||||
@Bean
|
||||
public ApplicationRunner adminUserSeedRunner(
|
||||
UserMapper userMapper,
|
||||
JdbcTemplate jdbcTemplate,
|
||||
IotdoorProperties properties,
|
||||
PasswordEncoder passwordEncoder
|
||||
) {
|
||||
return args -> {
|
||||
String adminUsername = properties.getSecurity().getAdminUsername();
|
||||
if (userMapper.countByUserId(adminUsername) > 0) {
|
||||
UserAccount existing = userMapper.selectByUserId(adminUsername);
|
||||
if (existing != null && !isBcryptHash(existing.getPswd())) {
|
||||
jdbcTemplate.update("""
|
||||
UPDATE TN_USER
|
||||
SET
|
||||
PSWD = ?,
|
||||
USER_NM = ?,
|
||||
USER_STTS_CD = ?,
|
||||
USER_TYPE_CD = ?,
|
||||
LOCK_YN = ?,
|
||||
LGN_FAIL_NMTM = 0,
|
||||
LOCK_DT = NULL
|
||||
WHERE USER_ID = ?
|
||||
""",
|
||||
passwordEncoder.encode(properties.getSecurity().getAdminPassword()),
|
||||
properties.getSecurity().getAdminDisplayName(),
|
||||
"A",
|
||||
"ADM",
|
||||
"N",
|
||||
adminUsername
|
||||
);
|
||||
LOGGER.info("Normalized legacy admin account for Spring Security login. username={}", adminUsername);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
UserAccount userAccount = new UserAccount();
|
||||
userAccount.setUserIdntfNo(UUID.randomUUID().toString().replace("-", ""));
|
||||
userAccount.setUserId(adminUsername);
|
||||
userAccount.setPswd(passwordEncoder.encode(properties.getSecurity().getAdminPassword()));
|
||||
userAccount.setUserNm(properties.getSecurity().getAdminDisplayName());
|
||||
userAccount.setEmlAddr("admin@iotdoor.local");
|
||||
userAccount.setUserSttsCd("A");
|
||||
userAccount.setUserTypeCd("ADM");
|
||||
userAccount.setJoinDt(LocalDateTime.now());
|
||||
userAccount.setIndvdlInfoAgreDt(LocalDateTime.now());
|
||||
userAccount.setLockYn("N");
|
||||
userAccount.setLgnFailNmtm(0);
|
||||
|
||||
if (hasLegacyColumns(jdbcTemplate)) {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO TN_USER (
|
||||
USER_IDNTF_NO,
|
||||
GROUP_IDNTF_NO,
|
||||
AUTHRT_CD,
|
||||
USER_ID,
|
||||
PSWD,
|
||||
USER_NM,
|
||||
EML_ADDR,
|
||||
USER_STTS_CD,
|
||||
USER_TYPE_CD,
|
||||
JOIN_DT,
|
||||
INDVDL_INFO_AGRE_DT,
|
||||
LOCK_YN,
|
||||
LGN_FAIL_NMTM,
|
||||
LOCK_DT,
|
||||
LAST_LGN_DT
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
userAccount.getUserIdntfNo(),
|
||||
"group-boot-default",
|
||||
"ROLE_ADMIN",
|
||||
userAccount.getUserId(),
|
||||
userAccount.getPswd(),
|
||||
userAccount.getUserNm(),
|
||||
userAccount.getEmlAddr(),
|
||||
userAccount.getUserSttsCd(),
|
||||
userAccount.getUserTypeCd(),
|
||||
userAccount.getJoinDt(),
|
||||
userAccount.getIndvdlInfoAgreDt(),
|
||||
userAccount.getLockYn(),
|
||||
userAccount.getLgnFailNmtm(),
|
||||
userAccount.getLockDt(),
|
||||
userAccount.getLastLgnDt()
|
||||
);
|
||||
} else {
|
||||
userMapper.insertUser(userAccount);
|
||||
}
|
||||
|
||||
LOGGER.info("Seeded default admin account into TN_USER. username={}", adminUsername);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean hasLegacyColumns(JdbcTemplate jdbcTemplate) {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'TN_USER'
|
||||
AND COLUMN_NAME IN ('GROUP_IDNTF_NO', 'AUTHRT_CD')
|
||||
""", Integer.class);
|
||||
return count != null && count == 2;
|
||||
}
|
||||
|
||||
private boolean isBcryptHash(String value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return value.startsWith("$2a$") || value.startsWith("$2b$") || value.startsWith("$2y$");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.UserMapper;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import kr.iotdoor.boot.model.user.UserAccount;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AdminUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public AdminUserDetailsService(UserMapper userMapper) {
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserAccount userAccount = userMapper.selectByUserId(username);
|
||||
if (userAccount == null) {
|
||||
throw new UsernameNotFoundException("관리자 계정을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
return new AdminUserPrincipal(userAccount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.ContactMapper;
|
||||
import kr.iotdoor.boot.model.contact.ContactRequest;
|
||||
import kr.iotdoor.boot.model.contact.ContactSubmission;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ContactSubmissionService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ContactSubmissionService.class);
|
||||
|
||||
private final ContactMapper contactMapper;
|
||||
|
||||
public ContactSubmissionService(ContactMapper contactMapper) {
|
||||
this.contactMapper = contactMapper;
|
||||
}
|
||||
|
||||
public ContactSubmission submit(ContactRequest request) {
|
||||
ContactSubmission submission = new ContactSubmission();
|
||||
submission.setContactIdntfNo(UUID.randomUUID().toString().replace("-", ""));
|
||||
submission.setName(request.name().trim());
|
||||
submission.setEmail(request.email().trim());
|
||||
submission.setPhone(request.phone().trim());
|
||||
submission.setSubject(request.subject().trim());
|
||||
submission.setMessage(request.message().trim());
|
||||
submission.setSubmittedAt(LocalDateTime.now());
|
||||
|
||||
contactMapper.insertContact(submission);
|
||||
|
||||
LOGGER.info("Contact inquiry received from {} <{}> / subject={}",
|
||||
submission.getName(), submission.getEmail(), submission.getSubject());
|
||||
|
||||
return submission;
|
||||
}
|
||||
|
||||
public List<ContactSubmission> recentSubmissions(int limit) {
|
||||
return contactMapper.selectRecentContacts(limit);
|
||||
}
|
||||
|
||||
public List<ContactSubmission> findAll() {
|
||||
return contactMapper.selectAllContacts();
|
||||
}
|
||||
|
||||
public int count() {
|
||||
return contactMapper.selectContactCount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.config.IotdoorProperties;
|
||||
import kr.iotdoor.boot.mapper.AtchFileDetailMapper;
|
||||
import kr.iotdoor.boot.mapper.AtchFileMapper;
|
||||
import kr.iotdoor.boot.model.file.StoredFile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class FileStorageService {
|
||||
|
||||
private static final DateTimeFormatter DATE_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
||||
|
||||
private final IotdoorProperties properties;
|
||||
private final AtchFileMapper atchFileMapper;
|
||||
private final AtchFileDetailMapper atchFileDetailMapper;
|
||||
|
||||
public FileStorageService(
|
||||
IotdoorProperties properties,
|
||||
AtchFileMapper atchFileMapper,
|
||||
AtchFileDetailMapper atchFileDetailMapper
|
||||
) {
|
||||
this.properties = properties;
|
||||
this.atchFileMapper = atchFileMapper;
|
||||
this.atchFileDetailMapper = atchFileDetailMapper;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public StoredFile storeSingleFile(MultipartFile multipartFile, String directory, String description) {
|
||||
if (multipartFile == null || multipartFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("업로드할 파일이 없습니다.");
|
||||
}
|
||||
|
||||
String atchFileIdntfNo = UUID.randomUUID().toString().replace("-", "");
|
||||
atchFileMapper.insertAtchFile(atchFileIdntfNo);
|
||||
|
||||
String sanitizedDirectory = sanitizeDirectory(directory);
|
||||
String relativeDirectory = sanitizedDirectory + "/" + LocalDate.now().format(DATE_PATH_FORMATTER);
|
||||
Path rootPath = Paths.get(properties.getFile().getUploadPath()).toAbsolutePath().normalize();
|
||||
Path storageDirectory = rootPath.resolve(relativeDirectory).normalize();
|
||||
|
||||
if (!storageDirectory.startsWith(rootPath)) {
|
||||
throw new IllegalArgumentException("허용되지 않은 업로드 경로입니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(storageDirectory);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("업로드 디렉터리를 생성할 수 없습니다.", exception);
|
||||
}
|
||||
|
||||
String originalFileName = StringUtils.cleanPath(multipartFile.getOriginalFilename());
|
||||
String extension = extractExtension(originalFileName);
|
||||
String storedFileName = UUID.randomUUID().toString().replace("-", "");
|
||||
if (StringUtils.hasText(extension)) {
|
||||
storedFileName += "." + extension;
|
||||
}
|
||||
|
||||
Path targetPath = storageDirectory.resolve(storedFileName).normalize();
|
||||
if (!targetPath.startsWith(rootPath)) {
|
||||
throw new IllegalArgumentException("허용되지 않은 파일 저장 위치입니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
multipartFile.transferTo(targetPath);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("파일 저장에 실패했습니다.", exception);
|
||||
}
|
||||
|
||||
StoredFile storedFile = new StoredFile();
|
||||
storedFile.setAtchFileIdntfNo(atchFileIdntfNo);
|
||||
storedFile.setFileSn(atchFileDetailMapper.selectAtchFileDtlNextSn(atchFileIdntfNo));
|
||||
storedFile.setFileStrgPathNm(relativeDirectory.replace('\\', '/'));
|
||||
storedFile.setFileStrgNm(storedFileName);
|
||||
storedFile.setOrgnlFileNm(originalFileName);
|
||||
storedFile.setFileExtnNm(extension);
|
||||
storedFile.setFileCn(description);
|
||||
storedFile.setFileSz(multipartFile.getSize());
|
||||
atchFileDetailMapper.insertAtchFileDtl(storedFile);
|
||||
storedFile.setUrl(toPublicUrl(atchFileIdntfNo, storedFile.getFileSn()));
|
||||
return storedFile;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public StoredFile getStoredFile(String atchFileIdntfNo, int fileSn) {
|
||||
StoredFile storedFile = atchFileDetailMapper.selectAtchFileDtl(atchFileIdntfNo, fileSn);
|
||||
if (storedFile != null) {
|
||||
storedFile.setUrl(toPublicUrl(atchFileIdntfNo, fileSn));
|
||||
}
|
||||
return storedFile;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Path resolvePath(StoredFile storedFile) {
|
||||
Path rootPath = Paths.get(properties.getFile().getUploadPath()).toAbsolutePath().normalize();
|
||||
Path targetPath = rootPath
|
||||
.resolve(storedFile.getFileStrgPathNm())
|
||||
.resolve(storedFile.getFileStrgNm())
|
||||
.normalize();
|
||||
|
||||
if (!targetPath.startsWith(rootPath)) {
|
||||
throw new IllegalArgumentException("허용되지 않은 파일 경로입니다.");
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
public String toPublicUrl(String atchFileIdntfNo, int fileSn) {
|
||||
return "/comn/file/view/" + atchFileIdntfNo + "/" + fileSn;
|
||||
}
|
||||
|
||||
private String sanitizeDirectory(String directory) {
|
||||
String value = StringUtils.hasText(directory) ? directory : "common";
|
||||
value = value.replace('\\', '/').replaceAll("[^a-zA-Z0-9/_-]", "");
|
||||
value = value.replaceAll("/+", "/");
|
||||
value = value.replaceAll("^/+", "");
|
||||
return StringUtils.hasText(value) ? value : "common";
|
||||
}
|
||||
|
||||
private String extractExtension(String fileName) {
|
||||
if (!StringUtils.hasText(fileName) || !fileName.contains(".")) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.UserMapper;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LoginAuditService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public LoginAuditService(UserMapper userMapper) {
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
|
||||
Object principal = event.getAuthentication().getPrincipal();
|
||||
if (principal instanceof AdminUserPrincipal adminUserPrincipal) {
|
||||
userMapper.updateLastLoginDt(adminUserPrincipal.getUserIdntfNo());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.model.admin.MigrationFeature;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MigrationCatalogService {
|
||||
|
||||
public List<MigrationFeature> getFeatures() {
|
||||
return List.of(
|
||||
new MigrationFeature(
|
||||
"공개 메인/정적 페이지",
|
||||
"운영 가능",
|
||||
"메인, 연락처, 포트폴리오와 공용 레이아웃을 Boot 3 템플릿으로 재배치했습니다.",
|
||||
"/, /index.do, /contact.do, /portfolio.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"관리자 인증",
|
||||
"DB 연동 완료",
|
||||
"TN_USER 기반으로 관리자 로그인/로그아웃과 BCrypt 비밀번호 검증을 연결했습니다.",
|
||||
"/comn/user/login/loginUser.do, /comn/user/login/actionLogin.do, /mfuc/index.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"문의 접수",
|
||||
"DB 저장 완료",
|
||||
"문의 API가 TN_CONTACT_INQRY 테이블에 저장되고 관리자 화면에서 최근 문의를 확인할 수 있습니다.",
|
||||
"/api/contact, /mfuc/contact/list.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"포트폴리오",
|
||||
"DB 연동 완료",
|
||||
"TN_PORTFOLIO와 첨부파일 모듈을 추가해 공개 목록/상세와 관리자 등록/수정/삭제를 구성했습니다.",
|
||||
"/portfolio.do, /portfolio/{id}, /mfuc/portfolio/list.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"파일 모듈",
|
||||
"기초 모듈 완료",
|
||||
"레거시 TN_ATCH_FILE / TN_ATCH_FILE_DTL 스키마를 따라 업로드 및 파일 조회 엔드포인트를 구성했습니다.",
|
||||
"/mfuc/file/upload.do, /comn/file/view/**"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.PortfolioMapper;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioForm;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioItem;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class PortfolioService {
|
||||
|
||||
private static final String FALLBACK_IMAGE = "/resources/kr/iotdoor/comn/site/layout/image/common/no_img.svg";
|
||||
|
||||
private final PortfolioMapper portfolioMapper;
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public PortfolioService(PortfolioMapper portfolioMapper, FileStorageService fileStorageService) {
|
||||
this.portfolioMapper = portfolioMapper;
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PortfolioItem> getPortfolioItems() {
|
||||
return portfolioMapper.selectPortfolioList().stream()
|
||||
.map(this::enrich)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PortfolioItem getPortfolioDetail(String portfolioIdntfNo) {
|
||||
if (portfolioMapper.incrementViewCount(portfolioIdntfNo) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PortfolioItem item = portfolioMapper.selectPortfolioDetail(portfolioIdntfNo);
|
||||
return item == null ? null : enrich(item);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PortfolioItem> getAdminPortfolioItems() {
|
||||
return portfolioMapper.selectPortfolioList().stream()
|
||||
.map(this::enrich)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PortfolioItem getPortfolioForEdit(String portfolioIdntfNo) {
|
||||
PortfolioItem item = portfolioMapper.selectPortfolioDetail(portfolioIdntfNo);
|
||||
return item == null ? null : enrich(item);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public int count() {
|
||||
return portfolioMapper.countPortfolio();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void save(PortfolioForm form, MultipartFile thumbnailFile, AdminUserPrincipal principal) {
|
||||
String thumbnailAtchFileIdntfNo = form.getThumbnailAtchFileIdntfNo();
|
||||
int thumbnailFileSn = 1;
|
||||
|
||||
if (thumbnailFile != null && !thumbnailFile.isEmpty()) {
|
||||
thumbnailAtchFileIdntfNo = fileStorageService.storeSingleFile(
|
||||
thumbnailFile,
|
||||
"portfolio/thumbnail",
|
||||
"portfolio-thumbnail"
|
||||
).getAtchFileIdntfNo();
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(thumbnailAtchFileIdntfNo)) {
|
||||
throw new IllegalArgumentException("대표 이미지는 필수입니다.");
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(form.getPortfolioIdntfNo())) {
|
||||
PortfolioItem existing = portfolioMapper.selectPortfolioDetail(form.getPortfolioIdntfNo());
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("수정할 포트폴리오를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
existing.setTitle(form.getTitle().trim());
|
||||
existing.setContent(form.getContent().trim());
|
||||
existing.setThumbnailAtchFileIdntfNo(thumbnailAtchFileIdntfNo);
|
||||
existing.setThumbnailFileSn(thumbnailFileSn);
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
portfolioMapper.updatePortfolio(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
PortfolioItem item = new PortfolioItem();
|
||||
item.setPortfolioIdntfNo(newIdentifier());
|
||||
item.setTitle(form.getTitle().trim());
|
||||
item.setContent(form.getContent().trim());
|
||||
item.setThumbnailAtchFileIdntfNo(thumbnailAtchFileIdntfNo);
|
||||
item.setThumbnailFileSn(thumbnailFileSn);
|
||||
item.setRegisteredAt(LocalDateTime.now());
|
||||
item.setRegisteredByUserIdntfNo(principal.getUserIdntfNo());
|
||||
portfolioMapper.insertPortfolio(item);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(String portfolioIdntfNo) {
|
||||
portfolioMapper.deletePortfolio(portfolioIdntfNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String resolveThumbnailUrl(String atchFileIdntfNo, int fileSn) {
|
||||
if (!StringUtils.hasText(atchFileIdntfNo)) {
|
||||
return FALLBACK_IMAGE;
|
||||
}
|
||||
return fileStorageService.toPublicUrl(atchFileIdntfNo, fileSn <= 0 ? 1 : fileSn);
|
||||
}
|
||||
|
||||
private PortfolioItem enrich(PortfolioItem item) {
|
||||
item.setSummary(summarize(item.getContent()));
|
||||
item.setImageUrl(resolveThumbnailUrl(item.getThumbnailAtchFileIdntfNo(), item.getThumbnailFileSn()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private String summarize(String content) {
|
||||
if (!StringUtils.hasText(content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String plainText = content
|
||||
.replaceAll("<[^>]*>", " ")
|
||||
.replace(" ", " ")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
|
||||
if (plainText.length() <= 120) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
return plainText.substring(0, 120) + "...";
|
||||
}
|
||||
|
||||
private String newIdentifier() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.service.ContactSubmissionService;
|
||||
import kr.iotdoor.boot.service.MigrationCatalogService;
|
||||
import kr.iotdoor.boot.service.PortfolioService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class AdminController {
|
||||
|
||||
private final ContactSubmissionService contactSubmissionService;
|
||||
private final MigrationCatalogService migrationCatalogService;
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public AdminController(
|
||||
ContactSubmissionService contactSubmissionService,
|
||||
MigrationCatalogService migrationCatalogService,
|
||||
PortfolioService portfolioService
|
||||
) {
|
||||
this.contactSubmissionService = contactSubmissionService;
|
||||
this.migrationCatalogService = migrationCatalogService;
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping({"/mfuc/index.do", "/mngr/index.do"})
|
||||
public String dashboard(Model model) {
|
||||
model.addAttribute("recentContacts", contactSubmissionService.recentSubmissions(10));
|
||||
model.addAttribute("contactCount", contactSubmissionService.count());
|
||||
model.addAttribute("portfolioCount", portfolioService.count());
|
||||
model.addAttribute("migrationFeatures", migrationCatalogService.getFeatures());
|
||||
return "kr/iotdoor/boot/admin/dashboard";
|
||||
}
|
||||
|
||||
@GetMapping("/mfuc/contact/list.do")
|
||||
public String contactList(Model model) {
|
||||
model.addAttribute("contacts", contactSubmissionService.findAll());
|
||||
return "kr/iotdoor/boot/admin/contact/list";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioForm;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioItem;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import kr.iotdoor.boot.service.PortfolioService;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Controller
|
||||
public class AdminPortfolioController {
|
||||
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public AdminPortfolioController(PortfolioService portfolioService) {
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping("/mfuc/portfolio/list.do")
|
||||
public String portfolioList(Model model) {
|
||||
model.addAttribute("portfolioItems", portfolioService.getAdminPortfolioItems());
|
||||
return "kr/iotdoor/boot/admin/portfolio/list";
|
||||
}
|
||||
|
||||
@GetMapping("/mfuc/portfolio/form.do")
|
||||
public String portfolioForm(
|
||||
@RequestParam(name = "portfolioIdntfNo", required = false) String portfolioIdntfNo,
|
||||
Model model
|
||||
) {
|
||||
PortfolioForm form = new PortfolioForm();
|
||||
if (portfolioIdntfNo != null && !portfolioIdntfNo.isBlank()) {
|
||||
PortfolioItem portfolio = portfolioService.getPortfolioForEdit(portfolioIdntfNo);
|
||||
if (portfolio != null) {
|
||||
form.setPortfolioIdntfNo(portfolio.getPortfolioIdntfNo());
|
||||
form.setTitle(portfolio.getTitle());
|
||||
form.setContent(portfolio.getContent());
|
||||
form.setThumbnailAtchFileIdntfNo(portfolio.getThumbnailAtchFileIdntfNo());
|
||||
model.addAttribute("currentThumbnailUrl", portfolio.getImageUrl());
|
||||
model.addAttribute("portfolioMeta", portfolio);
|
||||
}
|
||||
}
|
||||
|
||||
model.addAttribute("form", form);
|
||||
return "kr/iotdoor/boot/admin/portfolio/form";
|
||||
}
|
||||
|
||||
@PostMapping("/mfuc/portfolio/save.do")
|
||||
public String savePortfolio(
|
||||
@Valid @ModelAttribute("form") PortfolioForm form,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(name = "thumbnailFile", required = false) MultipartFile thumbnailFile,
|
||||
Authentication authentication,
|
||||
Model model
|
||||
) {
|
||||
if ((thumbnailFile == null || thumbnailFile.isEmpty()) && (form.getThumbnailAtchFileIdntfNo() == null || form.getThumbnailAtchFileIdntfNo().isBlank())) {
|
||||
bindingResult.rejectValue("thumbnailAtchFileIdntfNo", "required", "대표 이미지는 필수입니다.");
|
||||
}
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
if (form.getThumbnailAtchFileIdntfNo() != null && !form.getThumbnailAtchFileIdntfNo().isBlank()) {
|
||||
model.addAttribute("currentThumbnailUrl", portfolioService.resolveThumbnailUrl(form.getThumbnailAtchFileIdntfNo(), 1));
|
||||
}
|
||||
return "kr/iotdoor/boot/admin/portfolio/form";
|
||||
}
|
||||
|
||||
portfolioService.save(form, thumbnailFile, (AdminUserPrincipal) authentication.getPrincipal());
|
||||
return "redirect:/mfuc/portfolio/list.do?saved=1";
|
||||
}
|
||||
|
||||
@PostMapping("/mfuc/portfolio/delete.do")
|
||||
public String deletePortfolio(@RequestParam("portfolioIdntfNo") String portfolioIdntfNo) {
|
||||
portfolioService.delete(portfolioIdntfNo);
|
||||
return "redirect:/mfuc/portfolio/list.do?deleted=1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class AuthController {
|
||||
|
||||
@GetMapping("/comn/user/login/login.do")
|
||||
public String loginRedirect() {
|
||||
return "redirect:/comn/user/login/loginUser.do";
|
||||
}
|
||||
|
||||
@GetMapping("/comn/user/login/loginUser.do")
|
||||
public String loginPage(
|
||||
@RequestParam(name = "login_error", required = false) String loginError,
|
||||
Authentication authentication,
|
||||
HttpServletRequest request,
|
||||
Model model
|
||||
) {
|
||||
if (authentication != null
|
||||
&& authentication.isAuthenticated()
|
||||
&& !(authentication instanceof AnonymousAuthenticationToken)) {
|
||||
return "redirect:/mfuc/index.do";
|
||||
}
|
||||
|
||||
if (loginError != null) {
|
||||
model.addAttribute("loginMessage", "아이디 또는 비밀번호를 확인해주세요.");
|
||||
}
|
||||
|
||||
String referer = request.getHeader("referer");
|
||||
model.addAttribute("returnUrl", referer == null ? "/" : referer);
|
||||
return "kr/iotdoor/comn/user/login/lgnPge";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import kr.iotdoor.boot.model.contact.ContactRequest;
|
||||
import kr.iotdoor.boot.model.contact.ContactSubmission;
|
||||
import kr.iotdoor.boot.service.ContactSubmissionService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
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 java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/contact")
|
||||
public class ContactApiController {
|
||||
|
||||
private final ContactSubmissionService contactSubmissionService;
|
||||
|
||||
public ContactApiController(ContactSubmissionService contactSubmissionService) {
|
||||
this.contactSubmissionService = contactSubmissionService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> submit(@Valid @RequestBody ContactRequest request) {
|
||||
ContactSubmission submission = contactSubmissionService.submit(request);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("message", "문의가 정상적으로 접수되었습니다.");
|
||||
body.put("submittedAt", submission.getSubmittedAt().toString());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException exception) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("success", false);
|
||||
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
for (FieldError error : exception.getBindingResult().getFieldErrors()) {
|
||||
errors.putIfAbsent(error.getField(), error.getDefaultMessage());
|
||||
}
|
||||
|
||||
body.put("message", errors.values().stream().findFirst().orElse("입력값을 확인해주세요."));
|
||||
body.put("errors", errors);
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.model.file.StoredFile;
|
||||
import kr.iotdoor.boot.service.FileStorageService;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.StringUtils;
|
||||
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.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
|
||||
@Controller
|
||||
public class FileController {
|
||||
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public FileController(FileStorageService fileStorageService) {
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("/mfuc/file/upload.do")
|
||||
public ResponseEntity<Map<String, Object>> uploadFile(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(name = "directory", defaultValue = "common") String directory,
|
||||
@RequestParam(name = "description", required = false) String description
|
||||
) {
|
||||
StoredFile storedFile = fileStorageService.storeSingleFile(file, directory, description);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("atchFileIdntfNo", storedFile.getAtchFileIdntfNo());
|
||||
body.put("fileSn", storedFile.getFileSn());
|
||||
body.put("orgnlFileNm", storedFile.getOrgnlFileNm());
|
||||
body.put("url", storedFile.getUrl());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/comn/file/view/{atchFileIdntfNo}/{fileSn}")
|
||||
public ResponseEntity<UrlResource> viewFile(
|
||||
@PathVariable String atchFileIdntfNo,
|
||||
@PathVariable int fileSn
|
||||
) throws IOException {
|
||||
StoredFile storedFile = fileStorageService.getStoredFile(atchFileIdntfNo, fileSn);
|
||||
if (storedFile == null) {
|
||||
throw new ResponseStatusException(NOT_FOUND);
|
||||
}
|
||||
|
||||
Path filePath = fileStorageService.resolvePath(storedFile);
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new ResponseStatusException(NOT_FOUND);
|
||||
}
|
||||
|
||||
UrlResource resource = new UrlResource(filePath.toUri());
|
||||
String contentType = Files.probeContentType(filePath);
|
||||
MediaType mediaType = StringUtils.hasText(contentType)
|
||||
? MediaType.parseMediaType(contentType)
|
||||
: MediaType.APPLICATION_OCTET_STREAM;
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(mediaType)
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.inline()
|
||||
.filename(storedFile.getOrgnlFileNm(), StandardCharsets.UTF_8)
|
||||
.build()
|
||||
.toString()
|
||||
)
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.config.IotdoorProperties;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ControllerAdvice(annotations = Controller.class)
|
||||
public class GlobalViewModelAdvice {
|
||||
|
||||
private final IotdoorProperties properties;
|
||||
|
||||
public GlobalViewModelAdvice(IotdoorProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@ModelAttribute("siteInfo")
|
||||
public IotdoorProperties.Site siteInfo() {
|
||||
return properties.getSite();
|
||||
}
|
||||
|
||||
@ModelAttribute("popupList")
|
||||
public List<Object> popupList() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@ModelAttribute("subMainClass")
|
||||
public String subMainClass() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@ModelAttribute("currentAdminName")
|
||||
public String currentAdminName(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) {
|
||||
return "";
|
||||
}
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof AdminUserPrincipal adminUserPrincipal) {
|
||||
return adminUserPrincipal.getDisplayName();
|
||||
}
|
||||
return properties.getSecurity().getAdminDisplayName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.service.PortfolioService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class PublicPageController {
|
||||
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public PublicPageController(PortfolioService portfolioService) {
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping({"/", "/index.do"})
|
||||
public String home(Model model) {
|
||||
model.addAttribute("mainYn", "Y");
|
||||
return "kr/iotdoor/comn/site/mainPge";
|
||||
}
|
||||
|
||||
@GetMapping("/contact.do")
|
||||
public String contact() {
|
||||
return "kr/iotdoor/cmty/contact/contact";
|
||||
}
|
||||
|
||||
@GetMapping("/portfolio.do")
|
||||
public String portfolio(Model model) {
|
||||
model.addAttribute("portfolioItems", portfolioService.getPortfolioItems());
|
||||
return "kr/iotdoor/cmty/portfolio/portfolio";
|
||||
}
|
||||
|
||||
@GetMapping("/portfolio/{portfolioId}")
|
||||
public String portfolioDetail(@org.springframework.web.bind.annotation.PathVariable String portfolioId, Model model) {
|
||||
var portfolio = portfolioService.getPortfolioDetail(portfolioId);
|
||||
if (portfolio == null) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND,
|
||||
"포트폴리오를 찾을 수 없습니다."
|
||||
);
|
||||
}
|
||||
|
||||
model.addAttribute("portfolio", portfolio);
|
||||
return "kr/iotdoor/cmty/portfolio/detail";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
server:
|
||||
port: ${PORT:9090}
|
||||
servlet:
|
||||
session:
|
||||
timeout: 60m
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: iotdoor-boot
|
||||
devtools:
|
||||
restart:
|
||||
enabled: true
|
||||
poll-interval: 2s
|
||||
quiet-period: 500ms
|
||||
livereload:
|
||||
enabled: true
|
||||
thymeleaf:
|
||||
cache: false
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: ${IOTDOOR_MAX_FILE_SIZE:20MB}
|
||||
max-request-size: ${IOTDOOR_MAX_REQUEST_SIZE:100MB}
|
||||
datasource:
|
||||
# Legacy context.xml resource name: java:comp/env/jdbc/serviceDB
|
||||
url: ${IOTDOOR_DB_URL:jdbc:mariadb://localhost:3306/iotdoor2_db}
|
||||
username: ${IOTDOOR_DB_USERNAME:iotdoor_user}
|
||||
password: ${IOTDOOR_DB_PASSWORD:iotdoor_pass1!}
|
||||
driver-class-name: ${IOTDOOR_DB_DRIVER:org.mariadb.jdbc.Driver}
|
||||
hikari:
|
||||
pool-name: ${IOTDOOR_DB_POOL_NAME:IotdoorHikariPool}
|
||||
maximum-pool-size: ${IOTDOOR_DB_MAX_POOL_SIZE:20}
|
||||
minimum-idle: ${IOTDOOR_DB_MIN_IDLE:10}
|
||||
connection-test-query: SELECT 1
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
jackson:
|
||||
time-zone: Asia/Seoul
|
||||
mvc:
|
||||
problemdetails:
|
||||
enabled: true
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
|
||||
mybatis:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
iotdoor:
|
||||
site:
|
||||
name: 국제오토텍
|
||||
admin-title: 국제오토텍 관리자
|
||||
email: iotdoor1749@naver.com
|
||||
phone: 042-585-1749
|
||||
phone-alt: 010-3476-1749
|
||||
address: 대전광역시 서구 유등로 55 102호
|
||||
summary: 자동문 전문 업체로 철거, 시공, A/S까지 원스톱으로 대응합니다.
|
||||
description: 슬라이딩자동문, 산업용자동문, 내풍압셔터, 오버헤드도어를 중심으로 시공과 유지보수를 제공합니다.
|
||||
map-latitude: 36.2980733
|
||||
map-longitude: 127.3814560
|
||||
map-client-id: ${IOTDOOR_NAVER_MAP_CLIENT_ID:xkb35c3i3k}
|
||||
security:
|
||||
admin-username: ${IOTDOOR_ADMIN_USERNAME:admin}
|
||||
admin-password: ${IOTDOOR_ADMIN_PASSWORD:ChangeMe123!}
|
||||
admin-display-name: ${IOTDOOR_ADMIN_DISPLAY_NAME:관리자}
|
||||
file:
|
||||
upload-path: ${IOTDOOR_FILE_UPLOAD_PATH:./uploads}
|
||||
@@ -0,0 +1,71 @@
|
||||
CREATE TABLE IF NOT EXISTS TN_USER (
|
||||
USER_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
USER_ID VARCHAR(100) NOT NULL,
|
||||
PSWD VARCHAR(200) NOT NULL,
|
||||
USER_NM VARCHAR(100) NOT NULL,
|
||||
EML_ADDR VARCHAR(200) NOT NULL,
|
||||
USER_STTS_CD VARCHAR(10) NOT NULL DEFAULT 'A',
|
||||
USER_TYPE_CD VARCHAR(10) NOT NULL DEFAULT 'ADM',
|
||||
JOIN_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDVDL_INFO_AGRE_DT DATETIME NULL,
|
||||
LOCK_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||
LGN_FAIL_NMTM INT NULL DEFAULT 0,
|
||||
LOCK_DT DATETIME NULL,
|
||||
LAST_LGN_DT DATETIME NULL,
|
||||
PRIMARY KEY (USER_IDNTF_NO),
|
||||
UNIQUE (USER_ID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_ATCH_FILE (
|
||||
ATCH_FILE_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
CRT_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
DEL_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||
PRIMARY KEY (ATCH_FILE_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_ATCH_FILE_DTL (
|
||||
ATCH_FILE_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
FILE_SN INT NOT NULL,
|
||||
FILE_STRG_PATH_NM VARCHAR(500) NOT NULL,
|
||||
FILE_STRG_NM VARCHAR(255) NOT NULL,
|
||||
ORGNL_FILE_NM VARCHAR(255) NOT NULL,
|
||||
FILE_EXTN_NM VARCHAR(50) NULL,
|
||||
FILE_CN VARCHAR(255) NULL,
|
||||
FILE_SZ BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (ATCH_FILE_IDNTF_NO, FILE_SN),
|
||||
CONSTRAINT FK_TN_ATCH_FILE_DTL__MASTER FOREIGN KEY (ATCH_FILE_IDNTF_NO)
|
||||
REFERENCES TN_ATCH_FILE (ATCH_FILE_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_PORTFOLIO (
|
||||
PORTFOLIO_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
TITLE VARCHAR(200) NOT NULL,
|
||||
THUMBNAIL_ATCH_FILE_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
THUMBNAIL_FILE_SN INT NOT NULL DEFAULT 1,
|
||||
CONTENT TEXT NOT NULL,
|
||||
REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INQ_CNT BIGINT NOT NULL DEFAULT 0,
|
||||
REG_USER_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
DEL_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||
UPD_DT DATETIME NULL,
|
||||
PRIMARY KEY (PORTFOLIO_IDNTF_NO),
|
||||
CONSTRAINT FK_TN_PORTFOLIO__USER FOREIGN KEY (REG_USER_IDNTF_NO)
|
||||
REFERENCES TN_USER (USER_IDNTF_NO),
|
||||
CONSTRAINT FK_TN_PORTFOLIO__THUMBNAIL FOREIGN KEY (THUMBNAIL_ATCH_FILE_IDNTF_NO)
|
||||
REFERENCES TN_ATCH_FILE (ATCH_FILE_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_CONTACT_INQRY (
|
||||
CONTACT_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
NM VARCHAR(100) NOT NULL,
|
||||
EML_ADDR VARCHAR(200) NOT NULL,
|
||||
TELNO VARCHAR(30) NOT NULL,
|
||||
TTL VARCHAR(200) NOT NULL,
|
||||
CN TEXT NOT NULL,
|
||||
SUBMIT_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (CONTACT_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_TN_ATCH_FILE_DTL__PATH ON TN_ATCH_FILE_DTL (FILE_STRG_PATH_NM);
|
||||
CREATE INDEX IF NOT EXISTS IDX_TN_PORTFOLIO__REG_DT ON TN_PORTFOLIO (REG_DT);
|
||||
CREATE INDEX IF NOT EXISTS IDX_TN_CONTACT_INQRY__SUBMIT_DT ON TN_CONTACT_INQRY (SUBMIT_DT);
|
||||
@@ -0,0 +1,236 @@
|
||||
(function () {
|
||||
const EMAIL_JS_PUBLIC_KEY = 'd7I4hw_UMkVTo2UXy';
|
||||
const EMAIL_JS_SERVICE_ID = 'service_jfxkmcd';
|
||||
const EMAIL_JS_TEMPLATE_ID = 'template_vu49z25';
|
||||
|
||||
let emailJsInitialized = false;
|
||||
|
||||
function getCsrfHeader() {
|
||||
const token = document.querySelector('meta[name="_csrf"]');
|
||||
const header = document.querySelector('meta[name="_csrf_header"]');
|
||||
|
||||
return {
|
||||
token: token ? token.getAttribute('content') : '',
|
||||
header: header ? header.getAttribute('content') : ''
|
||||
};
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test((email || '').trim());
|
||||
}
|
||||
|
||||
function isValidMobile(phone) {
|
||||
return /^01[016789]-?\d{3,4}-?\d{4}$/.test((phone || '').trim());
|
||||
}
|
||||
|
||||
function onlyNumber(value) {
|
||||
return value.replace(/[^0-9]/g, '');
|
||||
}
|
||||
|
||||
function formatMobile(phone) {
|
||||
const numbers = onlyNumber(phone);
|
||||
|
||||
if (numbers.length <= 3) {
|
||||
return numbers;
|
||||
}
|
||||
if (numbers.length <= 7) {
|
||||
return numbers.replace(/(\d{3})(\d{1,4})/, '$1-$2');
|
||||
}
|
||||
if (numbers.length <= 11) {
|
||||
return numbers.replace(/(\d{3})(\d{3,4})(\d{1,4})/, '$1-$2-$3');
|
||||
}
|
||||
|
||||
return numbers.substring(0, 11).replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
|
||||
}
|
||||
|
||||
function setErrorVisible(id, visible) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function spinner(on) {
|
||||
const spinnerElement = $('#spinner');
|
||||
if (!spinnerElement.length) {
|
||||
return;
|
||||
}
|
||||
spinnerElement.toggleClass('show', on);
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
return {
|
||||
name: document.getElementById('name').value.trim(),
|
||||
email: document.getElementById('email').value.trim(),
|
||||
phone: document.getElementById('phone').value.trim(),
|
||||
subject: document.getElementById('subject').value.trim(),
|
||||
message: document.getElementById('message').value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
function validatePayload(payload) {
|
||||
if (!payload.name || !payload.email || !payload.phone || !payload.subject || !payload.message) {
|
||||
alert('필수 항목은 모두 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
if (!isValidEmail(payload.email)) {
|
||||
alert('올바른 이메일 형식이 아닙니다.');
|
||||
document.getElementById('email').focus();
|
||||
return false;
|
||||
}
|
||||
if (!isValidMobile(payload.phone)) {
|
||||
alert('올바른 휴대폰번호 형식이 아닙니다.');
|
||||
document.getElementById('phone').focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
['name', 'email', 'phone', 'subject', 'message'].forEach(function (id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.value = '';
|
||||
}
|
||||
});
|
||||
setErrorVisible('emailError', false);
|
||||
setErrorVisible('phoneError', false);
|
||||
}
|
||||
|
||||
function initEmailJs() {
|
||||
if (emailJsInitialized) {
|
||||
return true;
|
||||
}
|
||||
if (!window.emailjs || typeof window.emailjs.init !== 'function') {
|
||||
console.warn('EmailJS SDK could not be loaded.');
|
||||
return false;
|
||||
}
|
||||
|
||||
window.emailjs.init({
|
||||
publicKey: EMAIL_JS_PUBLIC_KEY
|
||||
});
|
||||
emailJsInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function sendContactToDatabase(payload) {
|
||||
const csrf = getCsrfHeader();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
$.ajax({
|
||||
url: '/api/contact',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload),
|
||||
beforeSend: function (xhr) {
|
||||
if (csrf.header && csrf.token) {
|
||||
xhr.setRequestHeader(csrf.header, csrf.token);
|
||||
}
|
||||
}
|
||||
}).done(function (response) {
|
||||
resolve(response || {});
|
||||
}).fail(function (xhr) {
|
||||
reject(xhr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEmailJsErrorMessage(error) {
|
||||
if (!error) {
|
||||
return 'Unknown EmailJS error';
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
if (error.text) {
|
||||
return error.text;
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch (ignored) {
|
||||
return 'Unknown EmailJS error';
|
||||
}
|
||||
}
|
||||
|
||||
function sendContactMail(form) {
|
||||
if (!initEmailJs() || !window.emailjs || typeof window.emailjs.sendForm !== 'function') {
|
||||
return Promise.reject(new Error('EmailJS SDK is not available.'));
|
||||
}
|
||||
|
||||
return window.emailjs.sendForm(
|
||||
EMAIL_JS_SERVICE_ID,
|
||||
EMAIL_JS_TEMPLATE_ID,
|
||||
form,
|
||||
{
|
||||
publicKey: EMAIL_JS_PUBLIC_KEY
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function sendContact(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const payload = buildPayload();
|
||||
if (!validatePayload(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSendMail');
|
||||
|
||||
spinner(true);
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await sendContactToDatabase(payload);
|
||||
|
||||
try {
|
||||
await sendContactMail(form);
|
||||
alert(response.message || '문의가 접수되었고 메일이 발송되었습니다.');
|
||||
} catch (error) {
|
||||
const errorMessage = getEmailJsErrorMessage(error);
|
||||
console.error('EmailJS send failed:', error);
|
||||
alert((response.message || '문의가 접수되었습니다.') + '\n메일 발송은 실패했습니다.\n사유: ' + errorMessage);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
} catch (xhr) {
|
||||
const response = xhr.responseJSON || {};
|
||||
alert(response.message || '문의 접수 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
spinner(false);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
const phoneInput = document.getElementById('phone');
|
||||
const emailInput = document.getElementById('email');
|
||||
const form = document.getElementById('contactForm');
|
||||
|
||||
initEmailJs();
|
||||
|
||||
if (phoneInput) {
|
||||
phoneInput.addEventListener('input', function (e) {
|
||||
e.target.value = formatMobile(e.target.value);
|
||||
});
|
||||
phoneInput.addEventListener('focusout', function () {
|
||||
setErrorVisible('phoneError', !!phoneInput.value && !isValidMobile(phoneInput.value));
|
||||
});
|
||||
}
|
||||
|
||||
if (emailInput) {
|
||||
emailInput.addEventListener('focusout', function () {
|
||||
setErrorVisible('emailError', !!emailInput.value && !isValidEmail(emailInput.value));
|
||||
});
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', sendContact);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
.login-container{
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login-container .login-box {
|
||||
align-content: center;
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
margin: auto 0;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 5px 6px rgba(0, 0, 0, .1019607843);
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
/********** Template CSS **********/
|
||||
:root {
|
||||
--primary: #254447;
|
||||
--light: #FFFFFF;
|
||||
--dark: #971f19;
|
||||
--gray: #9B9B9B;
|
||||
}
|
||||
|
||||
.fw-medium {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.fw-bold {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.fw-black {
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
display: none;
|
||||
right: 45px;
|
||||
bottom: 45px;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
|
||||
/*** Spinner ***/
|
||||
#spinner {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .5s ease-out, visibility 0s linear .5s;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
#spinner.show {
|
||||
transition: opacity .5s ease-out, visibility 0s linear 0s;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/*** Button ***/
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.btn.btn-primary,
|
||||
.btn.btn-outline-primary:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-square {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.btn-sm-square {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-lg-square {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.btn-square,
|
||||
.btn-sm-square,
|
||||
.btn-lg-square {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*** Navbar ***/
|
||||
.navbar.sticky-top {
|
||||
top: -100px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.navbar .navbar-brand,
|
||||
.navbar a.btn {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav .nav-link {
|
||||
margin-right: 30px;
|
||||
padding: 25px 0;
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav .nav-link:hover,
|
||||
.navbar .navbar-nav .nav-link.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar .dropdown-toggle::after {
|
||||
border: none;
|
||||
content: "\f107";
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
vertical-align: middle;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar .navbar-nav .nav-link {
|
||||
margin-right: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav {
|
||||
border-top: 1px solid #EEEEEE;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.navbar .nav-item .dropdown-menu {
|
||||
display: block;
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
top: 150%;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.navbar .nav-item:hover .dropdown-menu {
|
||||
top: 100%;
|
||||
visibility: visible;
|
||||
transition: .5s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Header ***/
|
||||
.owl-carousel-inner {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-carousel .owl-carousel-item {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.header-carousel .owl-carousel-item img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.header-carousel .owl-carousel-item p {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots .owl-dot {
|
||||
position: relative;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin: 5px 0;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 0 30px rgba(255, 255, 255, .9);
|
||||
border-radius: 45px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots .owl-dot.active {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots .owl-dot img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
padding: 2px;
|
||||
border-radius: 45px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(rgba(0, 0, 0, .1), rgba(0, 0, 0, .1)), url(../img/carousel-1.jpg) center center no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
|
||||
/*** About ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.about {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.about-text {
|
||||
padding-right: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.about-text {
|
||||
padding-right: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.about-text {
|
||||
padding-right: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Service ***/
|
||||
.service-item {
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
position: relative;
|
||||
margin: -50px 0 25px 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
background: #FFFFFF;
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.service-item:hover .service-icon {
|
||||
color: #FFFFFF;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
|
||||
/*** Feature ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.feature {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
padding-left: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.feature-text {
|
||||
padding-left: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.feature-text {
|
||||
padding-left: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Project Portfolio ***/
|
||||
#portfolio-flters li {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
color: var(--dark);
|
||||
cursor: pointer;
|
||||
transition: .5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
#portfolio-flters li:hover,
|
||||
#portfolio-flters li.active {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.portfolio-img {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio-img::before,
|
||||
.portfolio-img::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
background: var(--dark);
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.portfolio-img::before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.portfolio-img::after {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-img::before {
|
||||
width: 51%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-img::after {
|
||||
width: 51%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.portfolio-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-btn {
|
||||
opacity: 1;
|
||||
transition-delay: .3s;
|
||||
}
|
||||
|
||||
|
||||
/*** Quote ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.quote {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
padding-right: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.quote-text {
|
||||
padding-right: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.quote-text {
|
||||
padding-right: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Team ***/
|
||||
.team-item {
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.team-item img {
|
||||
border-radius: 8px 60px 0 0;
|
||||
}
|
||||
|
||||
.team-item .team-social {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
background: #FFFFFF;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
|
||||
/*** Testimonial ***/
|
||||
.testimonial-carousel::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.testimonial-carousel::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(to left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.testimonial-carousel::before,
|
||||
.testimonial-carousel::after {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.testimonial-carousel::before,
|
||||
.testimonial-carousel::after {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-nav {
|
||||
position: absolute;
|
||||
width: 350px;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
opacity: 0;
|
||||
transition: .5s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.testimonial-carousel:hover .owl-nav {
|
||||
width: 300px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-nav .owl-prev,
|
||||
.testimonial-carousel .owl-nav .owl-next {
|
||||
position: relative;
|
||||
color: var(--primary);
|
||||
font-size: 45px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-nav .owl-prev:hover,
|
||||
.testimonial-carousel .owl-nav .owl-next:hover {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.testimonial-carousel .testimonial-img img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.testimonial-carousel .testimonial-img .btn-square {
|
||||
position: absolute;
|
||||
bottom: -19px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-item .testimonial-text {
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
transform: scale(.8);
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-item.center .testimonial-text {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
/*** Contact ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.contact {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
padding-right: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.contact-text {
|
||||
padding-right: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.contact-text {
|
||||
padding-right: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Footer ***/
|
||||
.footer .btn.btn-social {
|
||||
margin-right: 5px;
|
||||
color: #9B9B9B;
|
||||
border: 1px solid #9B9B9B;
|
||||
border-radius: 38px;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.footer .btn.btn-social:hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--light);
|
||||
}
|
||||
|
||||
.footer .btn.btn-link {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: #9B9B9B;
|
||||
font-weight: normal;
|
||||
text-transform: capitalize;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.footer .btn.btn-link::before {
|
||||
position: relative;
|
||||
content: "\f105";
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.footer .btn.btn-link:hover {
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.footer .copyright {
|
||||
padding: 25px 0;
|
||||
border-top: 1px solid rgba(256, 256, 256, .1);
|
||||
}
|
||||
|
||||
.footer .copyright a {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.footer .copyright a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: #254447 !important;
|
||||
}
|
||||
.text-primary {
|
||||
color: #254447 !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #254447!important;
|
||||
border-color: #254447!important;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #254447!important;
|
||||
border-color: #254447!important;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
color: #ffffff!important;
|
||||
}
|
||||
/*
|
||||
a {
|
||||
color: #254447!important;
|
||||
}
|
||||
*/
|
||||
.bg-primary {
|
||||
background-color: #254447!important;
|
||||
}
|
||||
.btn.btn-primary{
|
||||
color: #ffffff!important;
|
||||
}
|
||||
.footer .btn.btn-social{
|
||||
color: var(--light)!important;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav .nav-link:hover, .navbar .navbar-nav .nav-link.active {
|
||||
color: var(--dark)!important;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
아래 부터 커스텀..
|
||||
*/
|
||||
|
||||
|
||||
.text-body {
|
||||
--bs-text-opacity: 1;
|
||||
color: var(--gray)!important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
|
||||
.site-sub-top {
|
||||
height: 483px;
|
||||
background: url(../image/template/backtop2.png) top center no-repeat;
|
||||
background-color: #194244;
|
||||
}
|
||||
|
||||
|
||||
/* 이미지 기본 설정 */
|
||||
.card-image {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
.album-list .card:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.album-list .card:hover img.card-image {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
.article-list .article-paging {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ag-paging-panel {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 MiB |
@@ -0,0 +1,21 @@
|
||||
<svg id="badge_ardor" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="51.836" height="42.288" viewBox="0 0 51.836 42.288">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_427" data-name="사각형 427" width="51.836" height="42.288" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1630" data-name="그룹 1630" clip-path="url(#clip-path)">
|
||||
<path id="패스_1092" data-name="패스 1092" d="M3.621,49.015a1.9,1.9,0,1,1-1.9,1.9,1.9,1.9,0,0,1,1.9-1.9" transform="translate(-0.459 -13.049)" fill="#d7deed"/>
|
||||
<path id="패스_1093" data-name="패스 1093" d="M64.557,7.664a2.529,2.529,0,1,1-2.529,2.529,2.528,2.528,0,0,1,2.529-2.529" transform="translate(-16.512 -2.041)" fill="#d7deed"/>
|
||||
<path id="패스_1094" data-name="패스 1094" d="M44.1,21.6a7.275,7.275,0,0,1,2.338-5.338A7.286,7.286,0,0,1,41.1,7.039,7.206,7.206,0,0,1,35.331,6.4a7.267,7.267,0,0,1-3.455-4.676A7.242,7.242,0,0,1,26.559,4.04a7.253,7.253,0,0,1-5.337-2.317A7.262,7.262,0,0,1,17.767,6.4,7.206,7.206,0,0,1,12,7.039a7.207,7.207,0,0,1-.641,5.771,7.26,7.26,0,0,1-4.676,3.455,7.283,7.283,0,0,1,0,10.654,7.268,7.268,0,0,1,4.676,3.454A7.21,7.21,0,0,1,12,36.146a7.287,7.287,0,0,1,9.227,5.337,7.237,7.237,0,0,1,10.653,0A7.287,7.287,0,0,1,41.1,36.146a7.286,7.286,0,0,1,5.337-9.227A7.263,7.263,0,0,1,44.1,21.6" transform="translate(-1.778 -0.459)" fill="#ffb95a"/>
|
||||
<path id="패스_1095" data-name="패스 1095" d="M30.191,15.367a9.857,9.857,0,1,1-9.868,9.868,9.862,9.862,0,0,1,9.868-9.868" transform="translate(-5.41 -4.091)" fill="#fcd884"/>
|
||||
<rect id="사각형_423" data-name="사각형 423" width="2.528" height="2.528" transform="translate(46.779 21.536) rotate(-45)"/>
|
||||
<rect id="사각형_424" data-name="사각형 424" width="2.528" height="2.528" transform="translate(44.474 39.236) rotate(-45)"/>
|
||||
<rect id="사각형_425" data-name="사각형 425" width="2.528" height="2.528" transform="translate(1.265 6.366) rotate(-45)"/>
|
||||
<rect id="사각형_426" data-name="사각형 426" width="2.528" height="2.528" transform="translate(0 30.387) rotate(-45)"/>
|
||||
<path id="패스_1096" data-name="패스 1096" d="M31.417,42.287a1.262,1.262,0,0,1-.927-.405,5.973,5.973,0,0,0-8.8,0,1.264,1.264,0,0,1-2.16-.577,6.017,6.017,0,0,0-5.859-4.668,6.16,6.16,0,0,0-1.772.26,1.264,1.264,0,0,1-1.571-1.586,5.991,5.991,0,0,0-4.391-7.619,1.265,1.265,0,0,1-.578-2.16,6,6,0,0,0,1.912-4.39A6.031,6.031,0,0,0,5.36,16.732a1.264,1.264,0,0,1,.579-2.159,5.991,5.991,0,0,0,4.39-7.617,1.264,1.264,0,0,1,1.582-1.582,6.038,6.038,0,0,0,1.79.272,5.932,5.932,0,0,0,2.972-.8A5.963,5.963,0,0,0,19.531.982,1.264,1.264,0,0,1,21.69.4,6.023,6.023,0,0,0,26.1,2.316,6,6,0,0,0,30.489.4a1.264,1.264,0,0,1,2.159.578,5.967,5.967,0,0,0,2.857,3.863,5.937,5.937,0,0,0,2.972.8,6.038,6.038,0,0,0,1.79-.272,1.264,1.264,0,0,1,1.586,1.571,6.019,6.019,0,0,0,4.406,7.628,1.265,1.265,0,0,1,.579,2.16,5.974,5.974,0,0,0,0,8.8,1.264,1.264,0,0,1-.577,2.16,6.02,6.02,0,0,0-4.428,7.558,1.285,1.285,0,0,1,.077.437,1.265,1.265,0,0,1-1.251,1.264,1.232,1.232,0,0,1-.379-.054,6.166,6.166,0,0,0-1.773-.261A6.018,6.018,0,0,0,32.649,41.3a1.262,1.262,0,0,1-1.232.983m-17.744-8.18a8.538,8.538,0,0,1,7.661,4.775,8.471,8.471,0,0,1,9.511,0,8.542,8.542,0,0,1,7.661-4.773q.291,0,.58.019a8.531,8.531,0,0,1,4.752-8.238,8.47,8.47,0,0,1,0-9.512,8.532,8.532,0,0,1-4.754-8.224q-.3.021-.608.021a8.457,8.457,0,0,1-4.24-1.141A8.6,8.6,0,0,1,30.844,3.4,8.544,8.544,0,0,1,26.1,4.845,8.592,8.592,0,0,1,21.336,3.4a8.6,8.6,0,0,1-3.394,3.632A8.466,8.466,0,0,1,13.7,8.174c-.2,0-.4-.007-.594-.021a8.5,8.5,0,0,1-1.118,4.832A8.6,8.6,0,0,1,8.357,16.38,8.594,8.594,0,0,1,9.8,21.143a8.55,8.55,0,0,1-1.443,4.745,8.6,8.6,0,0,1,3.632,3.392,8.508,8.508,0,0,1,1.118,4.847c.188-.012.377-.018.566-.018" transform="translate(-1.319 0)"/>
|
||||
<path id="패스_1097" data-name="패스 1097" d="M29.732,35.887a11.121,11.121,0,1,1,11.11-11.11,11.134,11.134,0,0,1-11.11,11.11m0-19.714a8.593,8.593,0,1,0,8.582,8.6,8.614,8.614,0,0,0-8.582-8.6" transform="translate(-4.952 -3.632)"/>
|
||||
<path id="패스_1098" data-name="패스 1098" d="M3.162,53.614a3.161,3.161,0,1,1,3.16-3.161,3.165,3.165,0,0,1-3.16,3.161m0-3.793a.632.632,0,1,0,.633.632.632.632,0,0,0-.633-.632" transform="translate(-0.001 -12.59)"/>
|
||||
<path id="패스_1099" data-name="패스 1099" d="M64.1,13.527a3.793,3.793,0,1,1,3.793-3.793A3.8,3.8,0,0,1,64.1,13.527m0-5.057a1.264,1.264,0,1,0,1.264,1.264A1.266,1.266,0,0,0,64.1,8.47" transform="translate(-16.054 -1.582)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg id="badge_ardor" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="55.287" height="34.029" viewBox="0 0 55.287 34.029">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_421" data-name="사각형 421" width="55.287" height="34.029" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1626" data-name="그룹 1626" clip-path="url(#clip-path)">
|
||||
<path id="패스_1073" data-name="패스 1073" d="M35.448,26.166V48c0,1.979,7.523,3.583,16.8,3.583s16.8-1.6,16.8-3.583V26.166Z" transform="translate(-24.607 -18.164)" fill="#36464f"/>
|
||||
<path id="패스_1074" data-name="패스 1074" d="M50.862,50.8c-6.023,0-17.413-.877-17.413-4.195V24.778a.611.611,0,0,1,.612-.612h33.6a.611.611,0,0,1,.612.612V46.609c0,3.318-11.39,4.195-17.414,4.195M34.672,25.389v21.22c0,1.009,5.715,2.971,16.19,2.971s16.191-1.963,16.191-2.971V25.389Z" transform="translate(-23.219 -16.775)" fill="#231815"/>
|
||||
<path id="패스_1075" data-name="패스 1075" d="M29.166,2.025,55.824,12.3A.374.374,0,0,1,55.8,13L29.141,21.177a.372.372,0,0,1-.219,0L2.264,13a.374.374,0,0,1-.025-.706L28.9,2.025a.374.374,0,0,1,.269,0" transform="translate(-1.388 -1.389)" fill="#4b616e"/>
|
||||
<path id="패스_1076" data-name="패스 1076" d="M111.843,43.934a.611.611,0,0,1-.612-.612V32.638L91.689,28.19A.612.612,0,0,1,91.96,27l20.018,4.556a.612.612,0,0,1,.476.6V43.322a.612.612,0,0,1-.612.612" transform="translate(-63.318 -18.73)" fill="#231815"/>
|
||||
<path id="패스_1077" data-name="패스 1077" d="M27.644,20.417a.986.986,0,0,1-.289-.044L.7,12.2a.986.986,0,0,1-.066-1.862L27.29.066a.982.982,0,0,1,.708,0L54.656,10.338a.985.985,0,0,1-.065,1.862L27.933,20.373a.981.981,0,0,1-.289.044M1.714,11.232l25.93,7.95,25.93-7.95L27.644,1.24Zm52.5.248h0ZM27.558,1.207h0" transform="translate(-0.001 0)" fill="#231815"/>
|
||||
<path id="패스_1078" data-name="패스 1078" d="M86.45,27.586c0,.784-1.389,1.42-3.1,1.42s-3.1-.636-3.1-1.42,1.389-1.42,3.1-1.42,3.1.636,3.1,1.42" transform="translate(-55.704 -18.164)" fill="#f7546a"/>
|
||||
<path id="패스_1079" data-name="패스 1079" d="M81.959,28.228c-2.152,0-3.714-.854-3.714-2.031s1.562-2.031,3.714-2.031,3.714.854,3.714,2.031-1.562,2.031-3.714,2.031m0-2.839c-1.634,0-2.491.6-2.491.808s.856.808,2.491.808,2.491-.6,2.491-.808-.857-.808-2.491-.808" transform="translate(-54.316 -16.775)" fill="#231815"/>
|
||||
<path id="패스_1080" data-name="패스 1080" d="M34.716,27.5l6.02-2.176c1.837-.664,1.045-3.62-.813-2.949L33.9,24.546c-1.837.664-1.045,3.62.813,2.949" transform="translate(-22.811 -15.461)" fill="#fff"/>
|
||||
<path id="패스_1081" data-name="패스 1081" d="M67.376,18.771a1.53,1.53,0,0,0,0-3.058,1.53,1.53,0,0,0,0,3.058" transform="translate(-45.745 -10.908)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg id="badge_atten" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="46.926" height="46.926" viewBox="0 0 46.926 46.926">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_430" data-name="사각형 430" width="46.926" height="46.926" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1638" data-name="그룹 1638" clip-path="url(#clip-path)">
|
||||
<path id="패스_1101" data-name="패스 1101" d="M47.519,24.759A22.759,22.759,0,1,1,24.759,2,22.759,22.759,0,0,1,47.519,24.759" transform="translate(-1.296 -1.296)" fill="#f0f0f0"/>
|
||||
<path id="패스_1102" data-name="패스 1102" d="M51.88,32.559A17.582,17.582,0,1,1,34.3,14.977,17.582,17.582,0,0,1,51.88,32.559" transform="translate(-10.835 -9.708)" fill="#6dc2f7"/>
|
||||
<path id="패스_1103" data-name="패스 1103" d="M51.46,28.738a17.581,17.581,0,1,0-27.8,17.814c11.415-2.21,21.9-8.217,27.8-17.814" transform="translate(-10.836 -9.708)" fill="#6dc2f7"/>
|
||||
<path id="패스_1104" data-name="패스 1104" d="M36.441,71.907a17.579,17.579,0,0,0,27.8-17.814c-5.9,9.6-16.389,15.6-27.8,17.814" transform="translate(-23.621 -35.063)" fill="#5ca2cf"/>
|
||||
<path id="패스_1105" data-name="패스 1105" d="M23.463,46.926A23.463,23.463,0,1,1,46.926,23.463,23.49,23.49,0,0,1,23.463,46.926m0-45.519A22.056,22.056,0,1,0,45.519,23.463,22.081,22.081,0,0,0,23.463,1.407" fill="#231815"/>
|
||||
<path id="패스_1106" data-name="패스 1106" d="M33,51.289A18.286,18.286,0,1,1,51.288,33,18.307,18.307,0,0,1,33,51.289m0-35.164A16.879,16.879,0,1,0,49.881,33,16.9,16.9,0,0,0,33,16.124" transform="translate(-9.539 -9.54)" fill="#231815"/>
|
||||
<path id="패스_1107" data-name="패스 1107" d="M49.4,53.416a.706.706,0,0,1-.282-.059l-8.685-3.8a.7.7,0,1,1,.564-1.289l8.347,3.652,10.809-6.69a.7.7,0,0,1,.741,1.2L49.766,53.311a.707.707,0,0,1-.37.105" transform="translate(-25.932 -29.25)" fill="#231815"/>
|
||||
<path id="패스_1108" data-name="패스 1108" d="M36.766,29.357c1.191-2.647,3.833-3.509,6.483-4.084,2.211-.481,1.275-3.873-.935-3.392-3.473.754-7.039,2.264-8.586,5.7-.922,2.048,2.109,3.839,3.038,1.776" transform="translate(-21.75 -14.153)" fill="#fff"/>
|
||||
<path id="패스_1109" data-name="패스 1109" d="M70.761,27.162a1.76,1.76,0,0,0,0-3.518,1.76,1.76,0,0,0,0,3.518" transform="translate(-44.766 -15.326)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,39 @@
|
||||
<svg id="badge_cooperation" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="59.738" height="34.392" viewBox="0 0 59.738 34.392">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_419" data-name="사각형 419" width="59.738" height="34.392" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1622" data-name="그룹 1622" clip-path="url(#clip-path)">
|
||||
<path id="패스_1028" data-name="패스 1028" d="M29.447,9.834a1.25,1.25,0,1,1-1.772,0,1.248,1.248,0,0,1,1.772,0" transform="translate(19.716 6.833)" fill="#bf542e"/>
|
||||
<path id="패스_1029" data-name="패스 1029" d="M8.242,9.349a1.269,1.269,0,0,1,.46,1.718,1.253,1.253,0,1,1-.46-1.718" transform="translate(4.595 6.633)" fill="#168f91"/>
|
||||
<path id="패스_1030" data-name="패스 1030" d="M29.109,3.1l5.035,8.723L37.5,17.633,30.72,21.541l-6.759-11.7-.126-.217-1.88-3.267-.15-.25L21.7,5.925l6.783-3.919.629,1.088ZM33.5,16.985a1.25,1.25,0,1,0-1.772,0,1.248,1.248,0,0,0,1.772,0" transform="translate(15.665 1.449)" fill="#bf542e"/>
|
||||
<path id="패스_1031" data-name="패스 1031" d="M33.023.285l7.451,12.9-7.022,4.055-2.416-4.183L26,4.336Z" transform="translate(18.773 0.206)" fill="#ff703d"/>
|
||||
<rect id="사각형_418" data-name="사각형 418" width="14.901" height="8.105" transform="translate(0.491 13.397) rotate(-60.013)" fill="#168f91"/>
|
||||
<path id="패스_1032" data-name="패스 1032" d="M13.086,1.658l6.783,3.909-.487.849-.028.024a.813.813,0,0,0-.071.064c-.022.019-.043.041-.062.06l-.007.01a.65.65,0,0,0-.065.074.75.75,0,0,0-.055.074l0,0a.85.85,0,0,0-.046.072.5.5,0,0,0-.033.059l-.021.038,0,.009a.609.609,0,0,0-.028.059.829.829,0,0,0-.048.129.947.947,0,0,0-.028.1l-.009.046a.578.578,0,0,0-.012.076l-1.744,3.022-4.53,7.838-1.462,2.538-.15.26-.129.222-.129-.077,0,0L4.068,17.285ZM10.36,16.5a1.252,1.252,0,1,0-2.166-1.255A1.252,1.252,0,1,0,10.36,16.5" transform="translate(2.937 1.197)" fill="#168f91"/>
|
||||
<path id="패스_1033" data-name="패스 1033" d="M19.8,20.1l-.539.935L16.8,22.463a1.959,1.959,0,0,1-2-3.368l-1.925,1.111a1.959,1.959,0,0,1-2-3.37L8.587,18.167a1.955,1.955,0,1,1-1.958-3.385l2.945-1.708.131.076.129-.222.222-.124,1.5,2.538Z" transform="translate(4.08 9.244)" fill="#fbd5a6"/>
|
||||
<path id="패스_1034" data-name="패스 1034" d="M18.955,19.607l-3,1.737a1.959,1.959,0,0,1-2-3.368l.539-.935,1.365.789,2.876,1.662a1.093,1.093,0,0,0,.133.071c.029.014.06.028.091.045" transform="translate(9.395 12.303)" fill="#fbd5a6"/>
|
||||
<path id="패스_1035" data-name="패스 1035" d="M35.018,16.52l1.471,2.548L20.635,9.551,20.6,9.532c-2.867.009-5.39-.281-7.143-1.922-.033-.031-.064-.064-.095-.095a1.968,1.968,0,0,1-.174-.189,3.308,3.308,0,0,1-.215-.289h0a2.055,2.055,0,0,1-.288-.684v0a1.371,1.371,0,0,1-.019-.449.571.571,0,0,1,.01-.076l.01-.046a.975.975,0,0,1,.026-.1.9.9,0,0,1,.048-.129c.009-.019.017-.04.028-.059l0-.009a.206.206,0,0,1,.021-.038c.01-.019.021-.038.033-.057s.031-.05.048-.074l0,0,.057-.074c.021-.024.041-.05.064-.074l.009-.009.06-.062c.024-.022.046-.043.071-.064l.028-.022c.833-.692,2.688-.992,5.781-.31,2.609.58,4.341-3.089,8.36.549l1.012-.3,2.032,3.516.124.219Z" transform="translate(9.136 2.609)" fill="#ddb992"/>
|
||||
<path id="패스_1036" data-name="패스 1036" d="M14.159,7.963l1.746-3.02a1.371,1.371,0,0,0,.019.449v0a1.965,1.965,0,0,0,.288.682l0,0a2.9,2.9,0,0,0,.215.288,1.968,1.968,0,0,0,.174.189c.029.031.062.064.095.095,1.753,1.641,4.276,1.93,7.143,1.922l15.888,9.536a2.246,2.246,0,0,1,.282,1.092,2.212,2.212,0,0,1-.294,1.109,2.254,2.254,0,0,1-3.046.82l-2.607-1.515a2.228,2.228,0,0,1-2.275,3.831l-2.194-1.264a2.228,2.228,0,0,1-2.276,3.831l-2.807-1.62a2.23,2.23,0,0,1-2.149,3.9c-.043-.021-.088-.045-.133-.071l-2.876-1.66-1.365-.79L9.741,21.014,8.167,18.34Z" transform="translate(5.896 3.569)" fill="#fbd5a6"/>
|
||||
<path id="패스_1037" data-name="패스 1037" d="M20.5,25.242a2.446,2.446,0,0,1-2.128-1.226,2.4,2.4,0,0,1-.307-1.507l-.813.47a2.459,2.459,0,0,1-3.344-.9,2.428,2.428,0,0,1-.3-1.515l-.279.16a2.448,2.448,0,0,1-3.654-2.407l-.635.368a2.445,2.445,0,0,1-3.583-2.769,2.412,2.412,0,0,1,1.138-1.469l2.939-1.7.014-.007a.492.492,0,0,1,.487.01l.129.077a.49.49,0,0,1-.363.9L7.081,15.3a1.439,1.439,0,0,0-.682.878,1.464,1.464,0,0,0,2.147,1.66l2.29-1.329a.49.49,0,0,1,.506.839,1.492,1.492,0,0,0-.508,1.994,1.47,1.47,0,0,0,2,.536l1.927-1.111a.49.49,0,0,1,.5.844,1.469,1.469,0,0,0,1.5,2.524L19.226,20.7a.49.49,0,0,1,.5.844,1.468,1.468,0,0,0,1.5,2.523L23.763,22.6a.458.458,0,0,1,.079-.229.489.489,0,0,1,.678-.141l.065.034.015.007.029.014c.022.012.046.022.071.036a.49.49,0,0,1,.019.859L21.714,24.92a2.449,2.449,0,0,1-1.216.322" transform="translate(3.874 9.151)"/>
|
||||
<path id="패스_1038" data-name="패스 1038" d="M10.009,16.036a.489.489,0,0,1-.422-.241L8.3,13.622a.48.48,0,0,1-.444-.25.489.489,0,0,1,.188-.666l.222-.126a.491.491,0,0,1,.663.177l1.5,2.538a.491.491,0,0,1-.422.74" transform="translate(5.628 9.037)"/>
|
||||
<path id="패스_1039" data-name="패스 1039" d="M23.55,29.221a2.705,2.705,0,0,1-1.2-.281c-.057-.028-.112-.057-.165-.09L9.7,21.644a.509.509,0,0,1-.177-.176L7.949,18.795a.489.489,0,0,1,0-.492l1.462-2.538L15.686,4.9a.49.49,0,0,1,.911.3.915.915,0,0,0,.014.3.491.491,0,0,1-.387.575.529.529,0,0,1-.091.009L8.939,18.543,10.3,20.859,22.68,28.005a1.112,1.112,0,0,0,.107.057,1.738,1.738,0,0,0,2.271-.689,1.757,1.757,0,0,0-.6-2.357.49.49,0,0,1,.5-.842l2.807,1.62a1.734,1.734,0,0,0,2.369-.63,1.759,1.759,0,0,0-.594-2.359.49.49,0,0,1,.5-.842l2.194,1.264a1.708,1.708,0,0,0,1.31.176,1.732,1.732,0,0,0,1.061-.808,1.757,1.757,0,0,0-.6-2.357.49.49,0,0,1,.5-.842l2.609,1.515a1.751,1.751,0,0,0,2.375-.642,1.729,1.729,0,0,0,.229-.863,1.751,1.751,0,0,0-.165-.744L23.8,9.205a.49.49,0,0,1,.479-.854l.034.019.012.007,15.856,9.517a.505.505,0,0,1,.176.181,2.719,2.719,0,0,1-3.728,3.687l-.858-.5a2.71,2.71,0,0,1-4.029,2.812l-.443-.255a2.709,2.709,0,0,1-4.028,2.822l-1.052-.608a2.707,2.707,0,0,1-2.674,3.187" transform="translate(5.69 3.364)"/>
|
||||
<path id="패스_1040" data-name="패스 1040" d="M13.367,6.67a.487.487,0,0,1-.339-.136l-.1-.1a.49.49,0,1,1,.694-.692c.029.028.059.057.088.086a.49.49,0,0,1-.339.844" transform="translate(9.227 4.04)"/>
|
||||
<path id="패스_1041" data-name="패스 1041" d="M13.261,6.587a.5.5,0,0,1-.375-.174,3.575,3.575,0,0,1-.248-.334.49.49,0,1,1,.816-.541c.059.088.119.167.181.243a.49.49,0,0,1-.374.806" transform="translate(9.064 3.839)"/>
|
||||
<path id="패스_1042" data-name="패스 1042" d="M13.166,6.583a.492.492,0,0,1-.413-.224,2.516,2.516,0,0,1-.356-.851.49.49,0,0,1,.961-.2,1.535,1.535,0,0,0,.22.515.492.492,0,0,1-.146.678.5.5,0,0,1-.265.077" transform="translate(8.942 3.552)"/>
|
||||
<path id="패스_1043" data-name="패스 1043" d="M11.053,21.889a.486.486,0,0,1-.245-.065L4.028,17.915a.489.489,0,0,1-.227-.3.484.484,0,0,1,.048-.372L12.866,1.619a.49.49,0,0,1,.67-.179l6.783,3.909a.49.49,0,0,1,.181.668l-.489.849a.489.489,0,1,1-.849-.487l.245-.425-5.936-3.42L4.942,17.311l5.932,3.42.033-.059,7.737-13.4a.49.49,0,0,1,.849.491L11.477,21.645a.481.481,0,0,1-.3.227.464.464,0,0,1-.127.017" transform="translate(2.731 0.992)"/>
|
||||
<path id="패스_1044" data-name="패스 1044" d="M12.872,5.655a.38.38,0,0,1-.088-.009.489.489,0,0,1-.394-.568l.009-.041,0-.014a1.478,1.478,0,0,1,.043-.16.49.49,0,1,1,.93.31.243.243,0,0,0-.01.038l-.009.041a.492.492,0,0,1-.482.4" transform="translate(8.939 3.27)"/>
|
||||
<path id="패스_1045" data-name="패스 1045" d="M12.92,5.452a.494.494,0,0,1-.184-.036.485.485,0,0,1-.269-.63.967.967,0,0,1,.043-.1.491.491,0,0,1,.876.441l-.01.022a.5.5,0,0,1-.456.3" transform="translate(8.975 3.191)"/>
|
||||
<path id="패스_1046" data-name="패스 1046" d="M12.939,5.411a.478.478,0,0,1-.2-.043.488.488,0,0,1-.25-.646.608.608,0,0,1,.06-.11.493.493,0,1,1,.387.8" transform="translate(8.988 3.172)"/>
|
||||
<path id="패스_1047" data-name="패스 1047" d="M12.969,5.37a.491.491,0,0,1-.429-.728c.024-.041.05-.081.079-.122a.49.49,0,1,1,.794.573l-.015.026a.491.491,0,0,1-.429.251" transform="translate(9.009 3.116)"/>
|
||||
<path id="패스_1048" data-name="패스 1048" d="M13,5.352a.473.473,0,0,1-.262-.077.484.484,0,0,1-.152-.668.988.988,0,0,1,.09-.122,1.2,1.2,0,0,1,.088-.1.49.49,0,1,1,.713.673.224.224,0,0,0-.033.038l-.01.012-.019.022A.5.5,0,0,1,13,5.352" transform="translate(9.031 3.053)"/>
|
||||
<path id="패스_1049" data-name="패스 1049" d="M13.073,5.255a.49.49,0,0,1-.358-.825c.026-.028.053-.057.083-.083s.062-.057.093-.084a.49.49,0,1,1,.629.752l-.038.034-.024.022-.026.026a.49.49,0,0,1-.358.157" transform="translate(9.085 2.997)"/>
|
||||
<path id="패스_1050" data-name="패스 1050" d="M7.824,12.385h0a1.757,1.757,0,0,1-.875-.234,1.738,1.738,0,0,1-.634-2.38,1.742,1.742,0,1,1,1.508,2.614m.005-2.5a.767.767,0,0,0-.744.959.747.747,0,0,0,.355.461.773.773,0,0,0,.384.1.758.758,0,0,0,.66-.379A.777.777,0,0,0,8.2,9.978a.762.762,0,0,0-.372-.1" transform="translate(4.389 6.427)"/>
|
||||
<path id="패스_1051" data-name="패스 1051" d="M33.659,17.94a.49.49,0,0,1-.425-.246L25.783,4.788a.489.489,0,0,1,.086-.6.484.484,0,0,1,.095-.071L32.984.067a.49.49,0,0,1,.67.179l7.453,12.9a.491.491,0,0,1-.181.67L33.9,17.873a.489.489,0,0,1-.245.067M26.876,4.723,33.838,16.78l6.173-3.564L33.049,1.16Z" transform="translate(18.566 0.001)"/>
|
||||
<path id="패스_1052" data-name="패스 1052" d="M36.694,19.762a.5.5,0,0,1-.253-.069L20.67,10.226h-.005c-2.69,0-5.426-.265-7.337-2.053-.041-.041-.079-.077-.114-.114a2.718,2.718,0,0,1-.2-.215,3.061,3.061,0,0,1-.251-.338l-.007-.01a2.481,2.481,0,0,1-.346-.82l-.01-.052a1.848,1.848,0,0,1-.021-.567A.776.776,0,0,1,12.4,5.94c0-.024.009-.045.014-.067a1.12,1.12,0,0,1,.036-.134,1.354,1.354,0,0,1,.069-.186c.009-.019.022-.05.036-.076a.558.558,0,0,1,.033-.064c.015-.029.033-.059.05-.088s.031-.048.052-.077l.024-.033c.017-.026.038-.052.057-.076l.01-.012c.022-.028.046-.055.071-.083l.031-.034.062-.06.01-.01c.026-.026.053-.05.081-.074l.036-.031c1.1-.907,3.3-1.054,6.2-.413a3.858,3.858,0,0,0,2.321-.456c1.5-.618,3.36-1.381,6.054.938l.752-.222a.5.5,0,0,1,.56.219l.15.25L31,8.423a.525.525,0,0,1,.048.117.447.447,0,0,1,.076.1l6,10.385a.489.489,0,0,1-.424.735M20.806,9.246a.478.478,0,0,1,.251.071l14.266,8.561L30.273,9.13a.532.532,0,0,1-.05-.117.578.578,0,0,1-.076-.1L28.315,5.728l-.649.191a.487.487,0,0,1-.467-.107c-2.349-2.127-3.749-1.551-5.231-.942a4.686,4.686,0,0,1-2.907.506c-3.642-.8-4.963-.121-5.36.208l-.01.009-.015.014c-.015.012-.029.026-.045.04l-.043.045h0l-.048.055L13.5,5.8l-.005.007-.024.038L13.44,5.9l0,0-.007.014-.015.031a.4.4,0,0,0-.026.069l-.007.022-.012.041-.007.034,0,.017,0,.04a.863.863,0,0,0,.012.288l0,.015a1.475,1.475,0,0,0,.217.5l.01.017a2.2,2.2,0,0,0,.167.222l.009.01a1.691,1.691,0,0,0,.139.152c.033.034.06.06.088.086,1.689,1.582,4.27,1.791,6.8,1.786Z" transform="translate(8.93 2.405)"/>
|
||||
<path id="패스_1053" data-name="패스 1053" d="M30.926,22.236a.488.488,0,0,1-.424-.245L21.481,6.374a.486.486,0,0,1,.179-.668l6.783-3.919a.49.49,0,0,1,.668.179l9.02,15.627a.493.493,0,0,1-.179.67L31.17,22.171a.478.478,0,0,1-.245.065M22.574,6.308,31.1,21.077l5.932-3.42L28.508,2.88Z" transform="translate(15.46 1.243)"/>
|
||||
<path id="패스_1054" data-name="패스 1054" d="M28.768,12.666A1.746,1.746,0,1,1,30,12.155a1.727,1.727,0,0,1-1.229.511m0-2.505a.757.757,0,0,0-.539.224.775.775,0,0,0-.224.542.745.745,0,0,0,.22.53.76.76,0,0,0,.542.227.767.767,0,0,0,.761-.758.759.759,0,0,0-.226-.542.743.743,0,0,0-.536-.224" transform="translate(19.51 6.629)"/>
|
||||
<path id="패스_1055" data-name="패스 1055" d="M7.51,17.939a.469.469,0,0,1-.245-.065L.245,13.82a.49.49,0,0,1-.179-.67L7.517.246a.483.483,0,0,1,.3-.229.492.492,0,0,1,.372.05L15.2,4.12a.49.49,0,0,1,.181.668L7.933,17.694a.489.489,0,0,1-.3.229.537.537,0,0,1-.127.015M1.159,13.215,7.331,16.78,14.287,4.723,8.119,1.16Z" transform="translate(0 0.001)"/>
|
||||
<path id="패스_1056" data-name="패스 1056" d="M27.84,4.465a.483.483,0,0,1-.329-.127c-2.345-2.128-3.749-1.55-5.233-.938a4.716,4.716,0,0,1-2.9.513c-3.568-.794-4.918-.143-5.333.172a.49.49,0,1,1-.594-.778c1.123-.858,3.3-.982,6.139-.351A3.86,3.86,0,0,0,21.9,2.494c1.541-.635,3.461-1.426,6.264,1.119a.489.489,0,0,1-.329.852" transform="translate(9.569 1.342)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg id="badge_cooperation" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34.83" height="39.384" viewBox="0 0 34.83 39.384">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_420" data-name="사각형 420" width="34.83" height="39.384" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1624" data-name="그룹 1624" clip-path="url(#clip-path)">
|
||||
<path id="패스_1057" data-name="패스 1057" d="M34.83,23.014v8.555a7.819,7.819,0,0,1-15.637,0v-8.1a7.263,7.263,0,0,1-1.767.223,7.439,7.439,0,0,1-1.789-.223v8.1A7.819,7.819,0,0,1,0,31.569V12.448A7.819,7.819,0,0,1,3.627,5.861V4.274A4.235,4.235,0,0,1,7.772,0l.086.682L7.865,0A4.235,4.235,0,0,1,12.01,4.274V5.861a7.809,7.809,0,0,1,3.627,6.587v.919a5.952,5.952,0,0,0,1.781.273,5.87,5.87,0,0,0,1.774-.273v-.919a7.8,7.8,0,0,1,3.678-6.616V4.274A4.235,4.235,0,0,1,27.015,0l.093.682V0A4.236,4.236,0,0,1,31.26,4.274V5.89a7.813,7.813,0,0,1,3.57,6.558V23.014Zm-1.35,4.159V23.287a8.809,8.809,0,0,0-6.465-2.593,9.267,9.267,0,0,0-5.4,1.645,7.271,7.271,0,0,1-1.063.632v4.2a7.812,7.812,0,0,1,12.929,0m0-5.732v-6.86A8.81,8.81,0,0,0,27.015,12a9.267,9.267,0,0,0-5.4,1.645,7.191,7.191,0,0,1-8.39,0A9.267,9.267,0,0,0,7.822,12a8.8,8.8,0,0,0-6.465,2.579v6.853a10.633,10.633,0,0,1,6.465-2.1,10.63,10.63,0,0,1,6.192,1.9,5.786,5.786,0,0,0,3.4,1.092h.007a5.754,5.754,0,0,0,3.4-1.092,11.012,11.012,0,0,1,12.664.2m0-8.677v-.316a6.465,6.465,0,0,0-3.3-5.631,6.561,6.561,0,0,0-5.524-.374.1.1,0,0,0-.043.014,6.389,6.389,0,0,0-.711.33,6.48,6.48,0,0,0-3.347,5.66v.259c.086-.057.18-.1.266-.158a10.652,10.652,0,0,1,6.2-1.9,10.542,10.542,0,0,1,6.465,2.119m0,18.805a6.465,6.465,0,1,0-6.465,6.465,6.474,6.474,0,0,0,6.465-6.465M29.9,5.186V4.274a2.873,2.873,0,0,0-2.8-2.916L27.022.682l.007.675a2.877,2.877,0,0,0-2.8,2.916v.869c.065-.022.136-.043.208-.065a3.963,3.963,0,0,1,.381-.129l.323-.086c.129-.029.259-.065.4-.086s.259-.043.388-.065.23-.036.338-.043a6.637,6.637,0,0,1,.754-.043,6.763,6.763,0,0,1,.761.043c.144.014.28.036.424.057.1.022.208.029.316.05.165.036.33.079.5.122.072.022.144.036.215.057.172.05.338.108.5.172.057.022.108.036.165.057M14.28,27.173v-4.2a7.2,7.2,0,0,1-1.056-.632,9.267,9.267,0,0,0-5.4-1.645,8.809,8.809,0,0,0-6.465,2.593v3.886a7.805,7.805,0,0,1,12.922,0m0-14.467v-.259a6.458,6.458,0,0,0-3.3-5.631,6.546,6.546,0,0,0-6.321,0,6.465,6.465,0,0,0-3.3,5.631v.316a10.542,10.542,0,0,1,6.465-2.119,10.63,10.63,0,0,1,6.192,1.9c.086.057.18.1.266.158m0,18.863a6.461,6.461,0,1,0-6.458,6.465,6.468,6.468,0,0,0,6.458-6.465m-3.62-26.4V4.274a2.873,2.873,0,0,0-2.8-2.916L7.779.682v.675a2.877,2.877,0,0,0-2.8,2.916v.891a1.448,1.448,0,0,1,.165-.05,3.906,3.906,0,0,1,.488-.165l.158-.043a7.89,7.89,0,0,1,2.033-.28,7.878,7.878,0,0,1,2.026.28,1.292,1.292,0,0,1,.158.043,3.906,3.906,0,0,1,.488.165,1.448,1.448,0,0,1,.165.05"/>
|
||||
<path id="패스_1058" data-name="패스 1058" d="M41.539,31.4v3.886a7.812,7.812,0,0,0-12.929,0v-4.2a7.271,7.271,0,0,0,1.063-.632,9.267,9.267,0,0,1,5.4-1.645A8.809,8.809,0,0,1,41.539,31.4" transform="translate(-8.059 -8.116)" fill="#ffae2b"/>
|
||||
<path id="패스_1059" data-name="패스 1059" d="M34.012,19.289v6.86a11.012,11.012,0,0,0-12.664-.2,5.754,5.754,0,0,1-3.4,1.092h-.007a5.786,5.786,0,0,1-3.4-1.092,10.63,10.63,0,0,0-6.192-1.9,10.633,10.633,0,0,0-6.465,2.1V19.289A8.8,8.8,0,0,1,8.355,16.71a9.267,9.267,0,0,1,5.4,1.645,7.191,7.191,0,0,0,8.39,0,9.267,9.267,0,0,1,5.4-1.645,8.81,8.81,0,0,1,6.465,2.579" transform="translate(-0.532 -4.707)" fill="#fff0bf"/>
|
||||
<path id="패스_1060" data-name="패스 1060" d="M41.539,14.8v.316A10.542,10.542,0,0,0,35.075,13a10.652,10.652,0,0,0-6.2,1.9c-.086.057-.18.1-.266.158V14.8a6.48,6.48,0,0,1,3.347-5.66,6.389,6.389,0,0,1,.711-.33.1.1,0,0,1,.043-.014,6.561,6.561,0,0,1,5.524.374,6.465,6.465,0,0,1,3.3,5.631" transform="translate(-8.059 -2.355)" fill="#ffae2b"/>
|
||||
<path id="패스_1061" data-name="패스 1061" d="M35.075,34.95a6.465,6.465,0,1,1-6.465,6.465,6.474,6.474,0,0,1,6.465-6.465m5.086,6.465A5.086,5.086,0,1,0,35.075,46.5a5.095,5.095,0,0,0,5.086-5.086" transform="translate(-8.059 -9.845)" fill="#50b1c2"/>
|
||||
<path id="패스_1062" data-name="패스 1062" d="M35.616,36.87a5.086,5.086,0,1,1-5.086,5.086,5.095,5.095,0,0,1,5.086-5.086m3.728,5.086a3.728,3.728,0,1,0-3.728,3.728,3.734,3.734,0,0,0,3.728-3.728" transform="translate(-8.6 -10.386)"/>
|
||||
<path id="패스_1063" data-name="패스 1063" d="M39.4,4.541v.912c-.057-.022-.108-.036-.165-.057-.165-.065-.33-.122-.5-.172-.072-.022-.144-.036-.215-.057-.172-.043-.338-.086-.5-.122-.108-.022-.215-.029-.316-.05-.144-.022-.28-.043-.424-.057a6.763,6.763,0,0,0-.761-.043,6.637,6.637,0,0,0-.754.043c-.108.007-.223.029-.338.043s-.259.036-.388.065-.266.057-.4.086l-.323.086a3.963,3.963,0,0,0-.381.129c-.072.022-.144.043-.208.065V4.541a2.877,2.877,0,0,1,2.8-2.916L36.524.95l.079.675a2.873,2.873,0,0,1,2.8,2.916" transform="translate(-9.502 -0.268)" fill="#a1f1ff"/>
|
||||
<path id="패스_1064" data-name="패스 1064" d="M36.148,38.76a3.728,3.728,0,1,1-3.728,3.728,3.734,3.734,0,0,1,3.728-3.728M37.7,41.806a.679.679,0,0,0-1-.919l-1.925,2.09a.672.672,0,0,0,.036.955.664.664,0,0,0,.46.18.685.685,0,0,0,.5-.215Z" transform="translate(-9.133 -10.919)" fill="#a1f1ff"/>
|
||||
<path id="패스_1065" data-name="패스 1065" d="M38.509,41.6a.673.673,0,0,1,.043.955l-1.925,2.09a.685.685,0,0,1-.5.215.664.664,0,0,1-.46-.18.672.672,0,0,1-.036-.955l1.925-2.09a.672.672,0,0,1,.955-.036" transform="translate(-9.985 -11.667)"/>
|
||||
<path id="패스_1066" data-name="패스 1066" d="M14.812,31.087v4.2a7.805,7.805,0,0,0-12.922,0V31.4A8.809,8.809,0,0,1,8.355,28.81a9.267,9.267,0,0,1,5.4,1.645,7.2,7.2,0,0,0,1.056.632" transform="translate(-0.532 -8.116)" fill="#ffae2b"/>
|
||||
<path id="패스_1067" data-name="패스 1067" d="M14.812,14.8v.259c-.086-.057-.18-.1-.266-.158A10.63,10.63,0,0,0,8.355,13,10.542,10.542,0,0,0,1.89,15.118V14.8a6.465,6.465,0,0,1,3.3-5.631,6.546,6.546,0,0,1,6.321,0,6.458,6.458,0,0,1,3.3,5.631" transform="translate(-0.532 -2.354)" fill="#ffae2b"/>
|
||||
<path id="패스_1068" data-name="패스 1068" d="M8.355,34.95A6.465,6.465,0,1,1,1.89,41.415,6.468,6.468,0,0,1,8.355,34.95m5.078,6.465A5.082,5.082,0,1,0,8.355,46.5a5.089,5.089,0,0,0,5.078-5.086" transform="translate(-0.532 -9.845)" fill="#50b1c2"/>
|
||||
<path id="패스_1069" data-name="패스 1069" d="M8.9,36.87A5.086,5.086,0,1,1,3.81,41.956,5.089,5.089,0,0,1,8.9,36.87m3.728,5.086A3.728,3.728,0,1,0,8.9,45.684a3.734,3.734,0,0,0,3.728-3.728" transform="translate(-1.073 -10.386)"/>
|
||||
<path id="패스_1070" data-name="패스 1070" d="M9.428,38.76A3.728,3.728,0,1,1,5.7,42.488,3.734,3.734,0,0,1,9.428,38.76m1.552,3.046a.679.679,0,0,0-1-.919l-1.925,2.09a.672.672,0,0,0,.036.955.678.678,0,0,0,.955-.036Z" transform="translate(-1.606 -10.919)" fill="#a1f1ff"/>
|
||||
<path id="패스_1071" data-name="패스 1071" d="M12.612,4.541v.891a1.448,1.448,0,0,0-.165-.05,3.906,3.906,0,0,0-.488-.165,1.292,1.292,0,0,0-.158-.043,7.878,7.878,0,0,0-2.026-.28,7.89,7.89,0,0,0-2.033.28l-.158.043a3.906,3.906,0,0,0-.488.165,1.448,1.448,0,0,0-.165.05V4.541a2.877,2.877,0,0,1,2.8-2.916V.95l.079.675a2.873,2.873,0,0,1,2.8,2.916" transform="translate(-1.952 -0.268)" fill="#a1f1ff"/>
|
||||
<path id="패스_1072" data-name="패스 1072" d="M11.789,41.6a.673.673,0,0,1,.043.955L9.9,44.644a.676.676,0,0,1-.991-.919l1.925-2.09a.672.672,0,0,1,.955-.036" transform="translate(-2.458 -11.667)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg id="badge_good" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="38.051" height="36.759" viewBox="0 0 38.051 36.759">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_431" data-name="사각형 431" width="38.051" height="36.759" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1655" data-name="그룹 1655" clip-path="url(#clip-path)">
|
||||
<path id="패스_1111" data-name="패스 1111" d="M139.275,21c3.9,0,3.81,5.81,0,5.81h-2.381c3.81.191,3.714,5.905-.1,5.905h-1.715c3.81.19,3.714,5.9-.1,5.9h-2.667c3.81.1,3.714,5.905-.1,5.905H117.941L114.6,42.168V21.4h1.152c4.858-1.333,5.429-7.065,5.525-9.827a5.994,5.994,0,0,1,.095-1.809,6.8,6.8,0,0,1,2.953-.381c3.714.856,3.619,9.714.857,11.618Z" transform="translate(-104.908 -8.561)" fill="#ffd8a8"/>
|
||||
<path id="패스_1112" data-name="패스 1112" d="M18.255,141.172v22.821h-8.9v-23.81h8.9Z" transform="translate(-8.565 -128.33)" fill="#407ed1"/>
|
||||
<path id="패스_1113" data-name="패스 1113" d="M123.661,36.76H109.376a.791.791,0,0,1-.457-.145l-3.343-2.361a.791.791,0,1,1,.913-1.292l3.138,2.216h14.034a2.04,2.04,0,0,0,2.114-2.148,2.017,2.017,0,0,0-2.038-2.175.791.791,0,0,1,.02-1.582h2.667a2.023,2.023,0,0,0,2.113-2.081,2.076,2.076,0,0,0-2.058-2.242.791.791,0,0,1,.04-1.581h1.715a2.023,2.023,0,0,0,2.113-2.081,2.076,2.076,0,0,0-2.058-2.243.791.791,0,0,1,.04-1.581h2.381a1.99,1.99,0,0,0,1.571-.649,2.279,2.279,0,0,0,.531-1.491,1.967,1.967,0,0,0-2.1-2.087h-14.1a.791.791,0,0,1-.449-1.443c1.311-.9,2.021-4.269,1.489-7.06-.159-.831-.675-2.792-2.05-3.131a6.568,6.568,0,0,0-2.094.194c-.009.317-.009.788-.009,1.22,0,.01,0,.018,0,.028a17.2,17.2,0,0,1-1.03,5.71,7.494,7.494,0,0,1-5.075,4.852.785.785,0,0,1-.21.028h-1.152a.791.791,0,0,1,0-1.582h1.042c3.036-.888,4.665-3.932,4.843-9.049,0-.526,0-.98.013-1.313a4.078,4.078,0,0,1,.037-.466.878.878,0,0,1,.516-.738v0a7.528,7.528,0,0,1,3.4-.438l.046.009c1.644.379,2.806,1.935,3.273,4.381a11.653,11.653,0,0,1-.9,7.217h12.4a3.56,3.56,0,0,1,3.684,3.67,3.585,3.585,0,0,1-3.136,3.689,3.946,3.946,0,0,1,.671,2.315,3.63,3.63,0,0,1-2.566,3.469,3.929,3.929,0,0,1,.756,2.434,3.6,3.6,0,0,1-3.39,3.613,3.956,3.956,0,0,1,.629,2.205,3.619,3.619,0,0,1-3.7,3.71M112.879,1.994c-.023,0-.047,0-.071,0,.024,0,.048,0,.071,0" transform="translate(-96.343 0)"/>
|
||||
<path id="패스_1114" data-name="패스 1114" d="M9.69,156.219H.791A.791.791,0,0,1,0,155.428v-23.81a.791.791,0,0,1,.791-.791h8.9a.791.791,0,0,1,.791.791v23.81a.791.791,0,0,1-.791.791m-8.109-1.582H8.9V132.409H1.582Z" transform="translate(0 -119.765)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg id="badge_sincerity" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="65.894" height="44.597" viewBox="0 0 65.894 44.597">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_422" data-name="사각형 422" width="60.64" height="26.287" transform="translate(0 0)" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1629" data-name="그룹 1629" transform="translate(0 19.742) rotate(-19)">
|
||||
<g id="그룹_1628" data-name="그룹 1628" transform="translate(0 0)" clip-path="url(#clip-path)">
|
||||
<path id="패스_1082" data-name="패스 1082" d="M169.987,2.233s6.583-1.949,8.555,4.51-4.578,8.519-4.578,8.519Z" transform="translate(-118.876 -1.369)" fill="#ffabbf"/>
|
||||
<path id="패스_1083" data-name="패스 1083" d="M2,66.808l7.865.819L7.892,61.163Z" transform="translate(-1.399 -42.773)" fill="#38322e"/>
|
||||
<path id="패스_1084" data-name="패스 1084" d="M36.043,51.141,33.3,44.854,27.58,42.092,21.6,47.826l1.973,6.464,7.989.831Z" transform="translate(-15.102 -29.436)" fill="#f7dccb"/>
|
||||
<path id="패스_1085" data-name="패스 1085" d="M8.466,66.829a.545.545,0,0,1-.062,0L.539,66.007a.6.6,0,0,1-.354-1.033L6.077,59.33a.6.6,0,0,1,.991.259l1.973,6.464a.6.6,0,0,1-.575.777M1.955,64.945l5.672.59L6.2,60.874Z" transform="translate(0 -41.374)" fill="#231815"/>
|
||||
<path id="패스_1086" data-name="패스 1086" d="M30.158,54.324c-.02,0-.041,0-.062,0l-7.988-.832a.6.6,0,0,1-.513-.422L19.621,46.6a.6.6,0,0,1,.159-.61l5.984-5.734a.6.6,0,0,1,.678-.107l5.716,2.762a.6.6,0,0,1,.289.3L35.195,49.5a.6.6,0,0,1-.152.691l-4.486,3.981a.6.6,0,0,1-.4.152m-7.529-1.99,7.328.763,3.961-3.515L31.435,43.9,26.3,41.417,20.88,46.606Z" transform="translate(-13.703 -28.037)" fill="#231815"/>
|
||||
<path id="패스_1087" data-name="패스 1087" d="M84.109,15.9,45.476,27.7l1.12-3.9-4.733-2.114,2.859-4.027L41.5,14.666,80.132,2.875Z" transform="translate(-29.021 -2.011)" fill="#f78f4a"/>
|
||||
<path id="패스_1088" data-name="패스 1088" d="M44.077,26.9a.6.6,0,0,1-.588-.726l.837-3.95-4.055-1.375a.6.6,0,0,1-.3-.918l2.553-3.6-2.835-2.624a.6.6,0,0,1,.233-1.017L78.558.9a.6.6,0,0,1,.751.4L83.286,14.33a.6.6,0,0,1-.4.751L44.253,26.872a.6.6,0,0,1-.176.026m-2.655-6.93,3.8,1.287a.6.6,0,0,1,.4.694l-.736,3.475L81.96,14.106,78.334,2.227,41.277,13.537l2.455,2.272a.6.6,0,0,1,.082.789Z" transform="translate(-27.623 -0.612)" fill="#231815"/>
|
||||
<path id="패스_1089" data-name="패스 1089" d="M50.819,25.292a.6.6,0,0,1-.175-1.177L87.161,12.969a.6.6,0,1,1,.351,1.15L50.995,25.266a.607.607,0,0,1-.176.026" transform="translate(-35.118 -9.051)" fill="#231815"/>
|
||||
<path id="패스_1090" data-name="패스 1090" d="M56.477,43.829a.6.6,0,0,1-.176-1.177L92.819,31.507a.6.6,0,0,1,.351,1.15L56.653,43.8a.607.607,0,0,1-.176.026" transform="translate(-39.075 -22.015)" fill="#231815"/>
|
||||
<path id="패스_1091" data-name="패스 1091" d="M172.566,14.5a.6.6,0,0,1-.576-.426L168.013,1.04a.6.6,0,0,1,.4-.752c.071-.021,7.18-2.038,9.3,4.912s-4.9,9.246-4.973,9.268a.588.588,0,0,1-.18.028M169.352,1.306l3.6,11.793c1.456-.644,5.076-2.77,3.617-7.548-1.464-4.8-5.65-4.528-7.216-4.244" transform="translate(-117.478 0)" fill="#231815"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="584.988" height="145.001" viewBox="0 0 584.988 145.001">
|
||||
<g id="그룹_6116" data-name="그룹 6116" transform="translate(16262.985 -10083.552)">
|
||||
<path id="교차_6" data-name="교차 6" d="M188.841-1298.9q.035-.663.035-1.336a25.136,25.136,0,0,0-25.108-25.107,25.135,25.135,0,0,0-25.106,25.107q0,.672.035,1.336H101.014q-.014-.667-.014-1.336a62.387,62.387,0,0,1,4.932-24.432,62.846,62.846,0,0,1,5.788-10.662,63.167,63.167,0,0,1,7.664-9.289,63.173,63.173,0,0,1,9.29-7.664,62.75,62.75,0,0,1,10.662-5.788A62.381,62.381,0,0,1,163.768-1363a62.382,62.382,0,0,1,24.432,4.932,62.818,62.818,0,0,1,10.662,5.788,63.181,63.181,0,0,1,9.289,7.664,63.23,63.23,0,0,1,7.665,9.289,62.841,62.841,0,0,1,5.787,10.662,62.372,62.372,0,0,1,4.933,24.432q0,.669-.014,1.336Z" transform="translate(-16363.985 11527.449)" fill="#fff" opacity="0.1"/>
|
||||
<path id="교차_7" data-name="교차 7" d="M250.96-1259.9a42.993,42.993,0,0,0-42.691-38.645,42.993,42.993,0,0,0-42.692,38.645H101.084a106.683,106.683,0,0,1,8.345-37.49,107.487,107.487,0,0,1,9.89-18.249,108.16,108.16,0,0,1,13.1-15.9,108.074,108.074,0,0,1,15.875-13.118,107.215,107.215,0,0,1,18.221-9.905A106.466,106.466,0,0,1,208.268-1363a106.462,106.462,0,0,1,41.753,8.443,107.215,107.215,0,0,1,18.221,9.905,108.035,108.035,0,0,1,15.875,13.118,108.16,108.16,0,0,1,13.1,15.9,107.551,107.551,0,0,1,9.89,18.249,106.683,106.683,0,0,1,8.345,37.49Z" transform="translate(-15576.913 8823.656) rotate(-180)" fill="#fff" opacity="0.1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg id="eraser" xmlns="http://www.w3.org/2000/svg" width="125.5" height="29" viewBox="0 0 125.5 29">
|
||||
<g id="그룹_4510" data-name="그룹 4510" transform="translate(-2337 -6920)">
|
||||
<g id="사각형_599" data-name="사각형 599" transform="translate(2337 6920)" fill="#54b175" stroke="#263b3a" stroke-width="3">
|
||||
<rect width="61" height="29" rx="5" stroke="none"/>
|
||||
<rect x="1.5" y="1.5" width="58" height="26" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
<g id="사각형_600" data-name="사각형 600" transform="translate(2363 6921)" fill="#fff" stroke="#263b3a" stroke-width="3">
|
||||
<rect width="10" height="27" rx="5" stroke="none"/>
|
||||
<rect x="1.5" y="1.5" width="7" height="24" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="사각형_1680" data-name="사각형 1680" transform="translate(125.5 19) rotate(90)" fill="#fff" stroke="#263b3a" stroke-width="3">
|
||||
<rect width="10" height="44" rx="5" stroke="none"/>
|
||||
<rect x="1.5" y="1.5" width="7" height="41" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |