초기 업로드

This commit is contained in:
CSKIM
2026-04-17 13:35:40 +09:00
commit e547089f66
265 changed files with 86707 additions and 0 deletions
@@ -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("&nbsp;", " ")
.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";
}
}
+71
View File
@@ -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}
+71
View File
@@ -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);
}
});
})();
File diff suppressed because one or more lines are too long
@@ -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);
}
File diff suppressed because one or more lines are too long
@@ -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;
}
File diff suppressed because it is too large Load Diff

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,41 @@
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,187 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="139.8" height="139.8" viewBox="0 0 139.8 139.8">
<defs>
<clipPath id="clip-path">
<path id="패스_191956" data-name="패스 191956" d="M147.5,83.1A69.9,69.9,0,1,1,77.6,13.2,69.906,69.906,0,0,1,147.5,83.1ZM77.6,35.5a47.7,47.7,0,1,0,47.7,47.7A47.692,47.692,0,0,0,77.6,35.5Z"/>
</clipPath>
<linearGradient id="linear-gradient" x1="0.146" y1="0.147" x2="0.853" y2="0.854" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f7f7f7"/>
<stop offset="0.237" stop-color="#f3f2f2"/>
<stop offset="0.529" stop-color="#e6e5e4"/>
<stop offset="0.85" stop-color="#d0cfce"/>
<stop offset="1" stop-color="#c3c2c2"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="0.146" y1="0.147" x2="0.853" y2="0.854" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#e6e6e5"/>
<stop offset="0.73" stop-color="#e2e1e1"/>
<stop offset="1" stop-color="#dededf"/>
</linearGradient>
<linearGradient id="linear-gradient-3" x1="0.147" y1="0.147" x2="0.853" y2="0.853" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#c3c2c2"/>
<stop offset="0.15" stop-color="#d0cfce"/>
<stop offset="0.471" stop-color="#e6e5e4"/>
<stop offset="0.763" stop-color="#f3f2f2"/>
<stop offset="1" stop-color="#f7f7f7"/>
</linearGradient>
<linearGradient id="linear-gradient-4" x1="0.146" y1="0.147" x2="0.853" y2="0.854" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#f2f2f2"/>
<stop offset="0.5" stop-color="#eeeeed"/>
<stop offset="1" stop-color="#e4e3e3"/>
</linearGradient>
<linearGradient id="linear-gradient-5" x1="-0.037" y1="1.071" x2="1.216" y2="-0.031" gradientUnits="objectBoundingBox">
<stop offset="0" stop-color="#efe7e4"/>
<stop offset="0.048" stop-color="#f3edeb"/>
<stop offset="0.205" stop-color="#fdfbfb"/>
<stop offset="0.332" stop-color="#fff"/>
<stop offset="0.433" stop-color="#fdfcfb"/>
<stop offset="0.554" stop-color="#f3edec"/>
<stop offset="0.598" stop-color="#efe7e4"/>
<stop offset="0.844" stop-color="#efe7e4"/>
<stop offset="0.942" stop-color="#ddd6cb"/>
<stop offset="1" stop-color="#d2ccba"/>
</linearGradient>
<linearGradient id="linear-gradient-6" x1="-68.175" y1="50.467" x2="-66.922" y2="49.391" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-7" x1="-155.83" y1="53.873" x2="-154.548" y2="52.821" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-8" x1="-240.643" y1="13.334" x2="-239.331" y2="12.283" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-9" x1="-283.06" y1="-61.352" x2="-281.793" y2="-62.404" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-10" x1="-292.39" y1="-153.667" x2="-291.123" y2="-154.744" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-11" x1="-255.109" y1="-240.422" x2="-253.857" y2="-241.523" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-12" x1="-189.104" y1="-284.172" x2="-187.837" y2="-285.249" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-13" x1="-105.205" y1="-282.284" x2="-103.924" y2="-283.335" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-14" x1="-26.699" y1="-241.76" x2="-25.387" y2="-242.811" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-15" x1="25.276" y1="-167.019" x2="26.558" y2="-168.07" xlink:href="#linear-gradient-5"/>
<linearGradient id="linear-gradient-16" x1="33.976" y1="-80.079" x2="35.229" y2="-81.154" xlink:href="#linear-gradient-5"/>
</defs>
<g id="frame_01" transform="translate(-7.7 -13.2)">
<g id="그룹_5369" data-name="그룹 5369" clip-path="url(#clip-path)">
<g id="그룹_5368" data-name="그룹 5368">
<circle id="타원_725" data-name="타원 725" cx="68.5" cy="68.5" r="68.5" transform="matrix(0.031, -0.999, 0.999, 0.031, 6.992, 149.471)" fill="#a7a7a6"/>
</g>
<circle id="타원_726" data-name="타원 726" cx="69.7" cy="69.7" r="69.7" transform="translate(7.9 13.4)" fill="url(#linear-gradient)"/>
<circle id="타원_727" data-name="타원 727" cx="68.5" cy="68.5" r="68.5" transform="translate(9.1 14.6)" fill="url(#linear-gradient-2)"/>
<circle id="타원_728" data-name="타원 728" cx="63.9" cy="63.9" r="63.9" transform="translate(13.7 19.2)" fill="url(#linear-gradient-3)"/>
<circle id="타원_729" data-name="타원 729" cx="62.7" cy="62.7" r="62.7" transform="translate(14.9 20.4)" fill="url(#linear-gradient-4)"/>
</g>
<g id="그룹_5418" data-name="그룹 5418" clip-path="url(#clip-path)">
<g id="그룹_5373" data-name="그룹 5373">
<g id="그룹_5370" data-name="그룹 5370">
<path id="패스_191957" data-name="패스 191957" d="M79.9,30.3l-2.7-1.5-2.7,1.4.5-3L72.9,25l3-.4,1.4-2.7,1.3,2.8,3.1.5-2.2,2.1Z" fill="url(#linear-gradient-5)"/>
</g>
<g id="그룹_5372" data-name="그룹 5372">
<g id="그룹_5371" data-name="그룹 5371">
<path id="패스_191958" data-name="패스 191958" d="M80.2,30.7l-3-1.6-3,1.5.6-3.3-2.4-2.4,3.4-.4,1.5-3,1.5,3.1,3.4.5-2.5,2.3Zm-3-2.1,2.4,1.3-.4-2.7,2-1.9-2.7-.4-1.2-2.5L76,24.9l-2.7.4,1.9,1.9-.5,2.7Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5377" data-name="그룹 5377">
<g id="그룹_5374" data-name="그룹 5374">
<path id="패스_191959" data-name="패스 191959" d="M52.8,36.1l-3.1.1L48,38.8l-1-2.9-3-.8,2.5-1.9-.2-3,2.5,1.7,2.9-1.1-.9,3Z" fill="url(#linear-gradient-6)"/>
</g>
<g id="그룹_5376" data-name="그룹 5376">
<g id="그룹_5375" data-name="그룹 5375">
<path id="패스_191960" data-name="패스 191960" d="M47.9,39.3l-1.1-3.2-3.3-.9,2.7-2.1L46,29.7l2.8,1.9L52,30.4l-1,3.3,2.1,2.6-3.4.1ZM44.5,35l2.7.7.9,2.6L49.6,36l2.8-.1-1.7-2.1.8-2.6-2.6,1-.1-.1-2.2-1.5.1,2.8Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5381" data-name="그룹 5381">
<g id="그룹_5378" data-name="그룹 5378">
<path id="패스_191961" data-name="패스 191961" d="M32.1,54.8l-2.6,1.6-.2,3.1-2.3-2-3,.8,1.2-2.9-1.7-2.6,3.1.3,2-2.4.7,3Z" fill="url(#linear-gradient-7)"/>
</g>
<g id="그룹_5380" data-name="그룹 5380">
<g id="그룹_5379" data-name="그룹 5379">
<path id="패스_191962" data-name="패스 191962" d="M29.5,59.9,27,57.7l-3.3.9L25,55.5l-1.8-2.9,3.4.3,2.1-2.6.8,3.3,3.2,1.2-2.9,1.8Zm-2.4-2.7h0L29.2,59l.1-2.8,2.3-1.4-2.6-1-.6-2.7-1.7,2.1L24,53.1l1.5,2.3v.1l-1,2.4Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5385" data-name="그룹 5385">
<g id="그룹_5382" data-name="그룹 5382">
<path id="패스_191963" data-name="패스 191963" d="M23.6,81.2l-1.5,2.7,1.4,2.8-3-.6-2.2,2.2-.4-3.1-2.7-1.4L18,82.5l.4-3,2.2,2.2Z" fill="url(#linear-gradient-8)"/>
</g>
<g id="그룹_5384" data-name="그룹 5384">
<g id="그룹_5383" data-name="그룹 5383">
<path id="패스_191964" data-name="패스 191964" d="M18.2,88.8l-.4-3.4-3-1.5,3.1-1.5.5-3.4,2.3,2.5L24,81l-1.6,3,1.5,3-3.3-.6Zm-2.5-4.9,2.5,1.3.4,2.7,2-1.9,2.7.5L22.1,84l1.3-2.4-2.9.4-1.9-2-.4,2.7Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5389" data-name="그룹 5389">
<g id="그룹_5386" data-name="그룹 5386">
<path id="패스_191965" data-name="패스 191965" d="M29.4,108.4l.1,3.1,2.6,1.7-2.9,1-.8,3-1.9-2.5-3.1.2,1.8-2.5-1.1-2.9,2.9.9Z" fill="url(#linear-gradient-9)"/>
</g>
<g id="그룹_5388" data-name="그룹 5388">
<g id="그룹_5387" data-name="그룹 5387">
<path id="패스_191966" data-name="패스 191966" d="M28.5,117.6l-2.1-2.7-3.4.2,1.9-2.8-1.2-3.2,3.3,1,2.6-2.1.1,3.4,2.8,1.9-3.2,1.1Zm-1.9-3.1,1.7,2.2L29,114l2.6-.9-2.3-1.5-.1-2.8-2.1,1.7-2.6-.8,1,2.6-.1.1-1.5,2.2Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5393" data-name="그룹 5393">
<g id="그룹_5390" data-name="그룹 5390">
<path id="패스_191967" data-name="패스 191967" d="M48.1,129l1.6,2.7,3,.1-2,2.4.8,2.9L48.7,136l-2.6,1.6.3-3-2.4-2,3-.7Z" fill="url(#linear-gradient-10)"/>
</g>
<g id="그룹_5392" data-name="그룹 5392">
<g id="그룹_5391" data-name="그룹 5391">
<path id="패스_191968" data-name="패스 191968" d="M45.9,138.1l.3-3.4-2.6-2.1,3.3-.8,1.2-3.2,1.8,2.9,3.4.2-2.2,2.6.9,3.3-3.1-1.3Zm-1.5-5.4,2.1,1.7-.2,2.7,2.3-1.5h.1l2.4,1-.7-2.7.1-.1,1.7-2-2.8-.2L48,129.3l-1,2.6Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5397" data-name="그룹 5397">
<g id="그룹_5394" data-name="그룹 5394">
<path id="패스_191969" data-name="패스 191969" d="M74.5,137.6l2.7,1.4,2.8-1.3-.6,3,2.2,2.2-3.1.4L77.1,146l-1.3-2.8-3-.5,2.2-2.1Z" fill="url(#linear-gradient-11)"/>
</g>
<g id="그룹_5396" data-name="그룹 5396">
<g id="그룹_5395" data-name="그룹 5395">
<path id="패스_191970" data-name="패스 191970" d="M77.1,146.5l-1.5-3.1-3.4-.5,2.5-2.3-.5-3.4,3,1.6,3-1.5-.6,3.3L82,143l-3.4.4Zm-3.9-3.9,2.8.4,1.2,2.5,1.3-2.5,2.7-.4-1.9-1.9.5-2.7-2.5,1.3L74.9,138l.4,2.7Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5401" data-name="그룹 5401">
<g id="그룹_5398" data-name="그룹 5398">
<path id="패스_191971" data-name="패스 191971" d="M101.7,131.7h3.1l1.7-2.6,1,2.9,2.9.8-2.4,1.9.2,3-2.6-1.7-2.8,1.1.8-3Z" fill="url(#linear-gradient-12)"/>
</g>
<g id="그룹_5400" data-name="그룹 5400">
<g id="그룹_5399" data-name="그룹 5399">
<path id="패스_191972" data-name="패스 191972" d="M108.4,138.2l-2.8-1.9-3.2,1.2,1-3.3-2.1-2.6,3.4-.1,1.9-2.8,1.1,3.2,3.3.9-2.7,2.1Zm-2.7-2.4h0l2.3,1.6-.1-2.8,2.2-1.7-2.7-.7-.9-2.6-1.5,2.3-2.8.1,1.7,2.1-.8,2.6Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5405" data-name="그룹 5405">
<g id="그룹_5402" data-name="그룹 5402">
<path id="패스_191973" data-name="패스 191973" d="M122.3,113.1l2.6-1.6.2-3.1,2.3,2,3-.8-1.2,2.9,1.7,2.6-3.1-.3-1.9,2.4-.7-3Z" fill="url(#linear-gradient-13)"/>
</g>
<g id="그룹_5404" data-name="그룹 5404">
<g id="그룹_5403" data-name="그룹 5403">
<path id="패스_191974" data-name="패스 191974" d="M125.8,117.7l-.8-3.3-3.2-1.2,2.9-1.8.2-3.4,2.6,2.2,3.3-.9-1.3,3.1,1.8,2.9-3.4-.3Zm-3-4.6,2.6,1v.1l.6,2.6,1.7-2.1,2.7.2-1.5-2.3v-.1l1-2.4-2.7.7-.1-.1-2-1.7-.1,2.8Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5409" data-name="그룹 5409">
<g id="그룹_5406" data-name="그룹 5406">
<path id="패스_191975" data-name="패스 191975" d="M130.9,86.7l1.4-2.7-1.4-2.8,3.1.6,2.1-2.2.4,3.1,2.8,1.4-2.8,1.3-.5,3-2.1-2.2Z" fill="url(#linear-gradient-14)"/>
</g>
<g id="그룹_5408" data-name="그룹 5408">
<g id="그룹_5407" data-name="그룹 5407">
<path id="패스_191976" data-name="패스 191976" d="M136.2,88.9l-2.3-2.5-3.3.5,1.6-3-1.5-3,3.3.6,2.4-2.4.4,3.4,3,1.5-3.1,1.5ZM134,86l1.9,2,.4-2.7,2.5-1.2-2.5-1.3-.4-2.7L134,82l-2.7-.5,1.2,2.5-1.3,2.4Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5413" data-name="그룹 5413">
<g id="그룹_5410" data-name="그룹 5410">
<path id="패스_191977" data-name="패스 191977" d="M125,59.5V56.4l-2.6-1.7,2.9-1,.8-3,1.8,2.5L131,53l-1.7,2.5,1.1,2.9-3-.9Z" fill="url(#linear-gradient-15)"/>
</g>
<g id="그룹_5412" data-name="그룹 5412">
<g id="그룹_5411" data-name="그룹 5411">
<path id="패스_191978" data-name="패스 191978" d="M124.8,59.9l-.1-3.4-2.8-1.9,3.2-1.1.9-3.3,2.1,2.7,3.4-.2-1.9,2.8,1.2,3.2-3.3-1Zm-2-5.1,2.3,1.5.1,2.8,2.1-1.7,2.6.8-1-2.6.1-.1,1.5-2.2-2.8.1L126,51.2l-.7,2.7Z" fill="#aeb4bb"/>
</g>
</g>
</g>
<g id="그룹_5417" data-name="그룹 5417">
<g id="그룹_5414" data-name="그룹 5414">
<path id="패스_191979" data-name="패스 191979" d="M106.4,38.9l-1.6-2.7-3.1-.1,2-2.4-.8-2.9,2.9,1.1,2.5-1.6-.2,3,2.4,2-3,.7Z" fill="url(#linear-gradient-16)"/>
</g>
<g id="그룹_5416" data-name="그룹 5416">
<g id="그룹_5415" data-name="그룹 5415">
<path id="패스_191980" data-name="패스 191980" d="M106.4,39.3l-1.8-2.9-3.4-.2,2.2-2.6-.9-3.3,3.1,1.3,2.9-1.8-.3,3.4,2.6,2.1-3.3.8Zm-4.2-3.4,2.8.1,1.4,2.3,1-2.6,2.7-.6L108,33.4l.2-2.7-2.3,1.5h-.1l-2.4-1,.7,2.7-.1.1Z" fill="#aeb4bb"/>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Some files were not shown because too many files have changed in this diff Show More