초기 업로드
@@ -0,0 +1,14 @@
|
||||
package kr.iotdoor.boot;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan
|
||||
public class IotdoorBootApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(IotdoorBootApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package kr.iotdoor.boot.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "iotdoor")
|
||||
public class IotdoorProperties {
|
||||
|
||||
private final Site site = new Site();
|
||||
private final Security security = new Security();
|
||||
private final File file = new File();
|
||||
|
||||
public Site getSite() {
|
||||
return site;
|
||||
}
|
||||
|
||||
public Security getSecurity() {
|
||||
return security;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public static class Site {
|
||||
private String name;
|
||||
private String adminTitle;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String phoneAlt;
|
||||
private String address;
|
||||
private String summary;
|
||||
private String description;
|
||||
private double mapLatitude;
|
||||
private double mapLongitude;
|
||||
private String mapClientId;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getAdminTitle() {
|
||||
return adminTitle;
|
||||
}
|
||||
|
||||
public void setAdminTitle(String adminTitle) {
|
||||
this.adminTitle = adminTitle;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getPhoneAlt() {
|
||||
return phoneAlt;
|
||||
}
|
||||
|
||||
public void setPhoneAlt(String phoneAlt) {
|
||||
this.phoneAlt = phoneAlt;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public double getMapLatitude() {
|
||||
return mapLatitude;
|
||||
}
|
||||
|
||||
public void setMapLatitude(double mapLatitude) {
|
||||
this.mapLatitude = mapLatitude;
|
||||
}
|
||||
|
||||
public double getMapLongitude() {
|
||||
return mapLongitude;
|
||||
}
|
||||
|
||||
public void setMapLongitude(double mapLongitude) {
|
||||
this.mapLongitude = mapLongitude;
|
||||
}
|
||||
|
||||
public String getMapClientId() {
|
||||
return mapClientId;
|
||||
}
|
||||
|
||||
public void setMapClientId(String mapClientId) {
|
||||
this.mapClientId = mapClientId;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Security {
|
||||
private String adminUsername;
|
||||
private String adminPassword;
|
||||
private String adminDisplayName;
|
||||
|
||||
public String getAdminUsername() {
|
||||
return adminUsername;
|
||||
}
|
||||
|
||||
public void setAdminUsername(String adminUsername) {
|
||||
this.adminUsername = adminUsername;
|
||||
}
|
||||
|
||||
public String getAdminPassword() {
|
||||
return adminPassword;
|
||||
}
|
||||
|
||||
public void setAdminPassword(String adminPassword) {
|
||||
this.adminPassword = adminPassword;
|
||||
}
|
||||
|
||||
public String getAdminDisplayName() {
|
||||
return adminDisplayName;
|
||||
}
|
||||
|
||||
public void setAdminDisplayName(String adminDisplayName) {
|
||||
this.adminDisplayName = adminDisplayName;
|
||||
}
|
||||
}
|
||||
|
||||
public static class File {
|
||||
private String uploadPath;
|
||||
|
||||
public String getUploadPath() {
|
||||
return uploadPath;
|
||||
}
|
||||
|
||||
public void setUploadPath(String uploadPath) {
|
||||
this.uploadPath = uploadPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package kr.iotdoor.boot.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(authorize -> authorize
|
||||
.requestMatchers(
|
||||
"/",
|
||||
"/index.do",
|
||||
"/portfolio.do",
|
||||
"/portfolio/**",
|
||||
"/contact.do",
|
||||
"/api/contact",
|
||||
"/comn/file/view/**",
|
||||
"/comn/user/login/login.do",
|
||||
"/comn/user/login/loginUser.do",
|
||||
"/resources/**",
|
||||
"/error",
|
||||
"/actuator/health"
|
||||
).permitAll()
|
||||
.requestMatchers("/mfuc/**", "/mngr/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/comn/user/login/loginUser.do")
|
||||
.loginProcessingUrl("/comn/user/login/actionLogin.do")
|
||||
.usernameParameter("id")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/mfuc/index.do", true)
|
||||
.failureUrl("/comn/user/login/loginUser.do?login_error=1")
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/comn/user/login/logout.do")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true)
|
||||
.deleteCookies("JSESSIONID")
|
||||
)
|
||||
.rememberMe(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.file.StoredFile;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface AtchFileDetailMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_ATCH_FILE_DTL (
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
FILE_SN,
|
||||
FILE_STRG_PATH_NM,
|
||||
FILE_STRG_NM,
|
||||
ORGNL_FILE_NM,
|
||||
FILE_EXTN_NM,
|
||||
FILE_CN,
|
||||
FILE_SZ
|
||||
) VALUES (
|
||||
#{atchFileIdntfNo},
|
||||
#{fileSn},
|
||||
#{fileStrgPathNm},
|
||||
#{fileStrgNm},
|
||||
#{orgnlFileNm},
|
||||
#{fileExtnNm},
|
||||
#{fileCn},
|
||||
#{fileSz}
|
||||
)
|
||||
""")
|
||||
int insertAtchFileDtl(StoredFile storedFile);
|
||||
|
||||
@Select("""
|
||||
SELECT COALESCE(MAX(FILE_SN), 0) + 1 AS FILE_SN
|
||||
FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
""")
|
||||
int selectAtchFileDtlNextSn(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
FILE_SN,
|
||||
FILE_STRG_PATH_NM,
|
||||
FILE_STRG_NM,
|
||||
ORGNL_FILE_NM,
|
||||
FILE_EXTN_NM,
|
||||
FILE_CN,
|
||||
COALESCE(FILE_SZ, 0) AS FILE_SZ
|
||||
FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
""")
|
||||
List<StoredFile> selectAtchFileDtlList(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
FILE_SN,
|
||||
FILE_STRG_PATH_NM,
|
||||
FILE_STRG_NM,
|
||||
ORGNL_FILE_NM,
|
||||
FILE_EXTN_NM,
|
||||
FILE_CN,
|
||||
COALESCE(FILE_SZ, 0) AS FILE_SZ
|
||||
FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
AND FILE_SN = #{fileSn}
|
||||
""")
|
||||
StoredFile selectAtchFileDtl(@Param("atchFileIdntfNo") String atchFileIdntfNo, @Param("fileSn") int fileSn);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM TN_ATCH_FILE_DTL
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
AND FILE_SN = #{fileSn}
|
||||
""")
|
||||
int deleteAtchFileDtl(@Param("atchFileIdntfNo") String atchFileIdntfNo, @Param("fileSn") int fileSn);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface AtchFileMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_ATCH_FILE (
|
||||
ATCH_FILE_IDNTF_NO,
|
||||
CRT_DT,
|
||||
DEL_YN
|
||||
) VALUES (
|
||||
#{atchFileIdntfNo},
|
||||
CURRENT_TIMESTAMP,
|
||||
'N'
|
||||
)
|
||||
""")
|
||||
int insertAtchFile(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_ATCH_FILE SET
|
||||
DEL_YN = 'Y'
|
||||
WHERE ATCH_FILE_IDNTF_NO = #{atchFileIdntfNo}
|
||||
""")
|
||||
int deleteAtchFile(@Param("atchFileIdntfNo") String atchFileIdntfNo);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.contact.ContactSubmission;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ContactMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_CONTACT_INQRY (
|
||||
CONTACT_IDNTF_NO,
|
||||
NM,
|
||||
EML_ADDR,
|
||||
TELNO,
|
||||
TTL,
|
||||
CN,
|
||||
SUBMIT_DT
|
||||
) VALUES (
|
||||
#{contactIdntfNo},
|
||||
#{name},
|
||||
#{email},
|
||||
#{phone},
|
||||
#{subject},
|
||||
#{message},
|
||||
CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
int insertContact(ContactSubmission contactSubmission);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
CONTACT_IDNTF_NO,
|
||||
NM AS NAME,
|
||||
EML_ADDR AS EMAIL,
|
||||
TELNO AS PHONE,
|
||||
TTL AS SUBJECT,
|
||||
CN AS MESSAGE,
|
||||
SUBMIT_DT AS SUBMITTED_AT
|
||||
FROM TN_CONTACT_INQRY
|
||||
ORDER BY SUBMIT_DT DESC
|
||||
LIMIT #{limit}
|
||||
""")
|
||||
List<ContactSubmission> selectRecentContacts(@Param("limit") int limit);
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
CONTACT_IDNTF_NO,
|
||||
NM AS NAME,
|
||||
EML_ADDR AS EMAIL,
|
||||
TELNO AS PHONE,
|
||||
TTL AS SUBJECT,
|
||||
CN AS MESSAGE,
|
||||
SUBMIT_DT AS SUBMITTED_AT
|
||||
FROM TN_CONTACT_INQRY
|
||||
ORDER BY SUBMIT_DT DESC
|
||||
""")
|
||||
List<ContactSubmission> selectAllContacts();
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM TN_CONTACT_INQRY
|
||||
""")
|
||||
int selectContactCount();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioItem;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface PortfolioMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
p.PORTFOLIO_IDNTF_NO,
|
||||
p.TITLE,
|
||||
p.CONTENT,
|
||||
p.THUMBNAIL_ATCH_FILE_IDNTF_NO,
|
||||
p.THUMBNAIL_FILE_SN,
|
||||
p.REG_DT AS REGISTERED_AT,
|
||||
p.INQ_CNT AS VIEW_COUNT,
|
||||
p.REG_USER_IDNTF_NO AS REGISTERED_BY_USER_IDNTF_NO,
|
||||
u.USER_NM AS REGISTERED_BY_NAME,
|
||||
p.DEL_YN,
|
||||
p.UPD_DT AS UPDATED_AT
|
||||
FROM TN_PORTFOLIO p
|
||||
JOIN TN_USER u ON u.USER_IDNTF_NO = p.REG_USER_IDNTF_NO
|
||||
WHERE p.DEL_YN = 'N'
|
||||
ORDER BY p.REG_DT DESC
|
||||
""")
|
||||
List<PortfolioItem> selectPortfolioList();
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
p.PORTFOLIO_IDNTF_NO,
|
||||
p.TITLE,
|
||||
p.CONTENT,
|
||||
p.THUMBNAIL_ATCH_FILE_IDNTF_NO,
|
||||
p.THUMBNAIL_FILE_SN,
|
||||
p.REG_DT AS REGISTERED_AT,
|
||||
p.INQ_CNT AS VIEW_COUNT,
|
||||
p.REG_USER_IDNTF_NO AS REGISTERED_BY_USER_IDNTF_NO,
|
||||
u.USER_NM AS REGISTERED_BY_NAME,
|
||||
p.DEL_YN,
|
||||
p.UPD_DT AS UPDATED_AT
|
||||
FROM TN_PORTFOLIO p
|
||||
JOIN TN_USER u ON u.USER_IDNTF_NO = p.REG_USER_IDNTF_NO
|
||||
WHERE p.PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
AND p.DEL_YN = 'N'
|
||||
""")
|
||||
PortfolioItem selectPortfolioDetail(@Param("portfolioIdntfNo") String portfolioIdntfNo);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_PORTFOLIO (
|
||||
PORTFOLIO_IDNTF_NO,
|
||||
TITLE,
|
||||
THUMBNAIL_ATCH_FILE_IDNTF_NO,
|
||||
THUMBNAIL_FILE_SN,
|
||||
CONTENT,
|
||||
REG_DT,
|
||||
INQ_CNT,
|
||||
REG_USER_IDNTF_NO,
|
||||
DEL_YN,
|
||||
UPD_DT
|
||||
) VALUES (
|
||||
#{portfolioIdntfNo},
|
||||
#{title},
|
||||
#{thumbnailAtchFileIdntfNo},
|
||||
#{thumbnailFileSn},
|
||||
#{content},
|
||||
#{registeredAt},
|
||||
0,
|
||||
#{registeredByUserIdntfNo},
|
||||
'N',
|
||||
NULL
|
||||
)
|
||||
""")
|
||||
int insertPortfolio(PortfolioItem portfolioItem);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_PORTFOLIO
|
||||
SET
|
||||
TITLE = #{title},
|
||||
THUMBNAIL_ATCH_FILE_IDNTF_NO = #{thumbnailAtchFileIdntfNo},
|
||||
THUMBNAIL_FILE_SN = #{thumbnailFileSn},
|
||||
CONTENT = #{content},
|
||||
UPD_DT = #{updatedAt}
|
||||
WHERE PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
AND DEL_YN = 'N'
|
||||
""")
|
||||
int updatePortfolio(PortfolioItem portfolioItem);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_PORTFOLIO
|
||||
SET
|
||||
DEL_YN = 'Y',
|
||||
UPD_DT = CURRENT_TIMESTAMP
|
||||
WHERE PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
""")
|
||||
int deletePortfolio(@Param("portfolioIdntfNo") String portfolioIdntfNo);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_PORTFOLIO
|
||||
SET INQ_CNT = INQ_CNT + 1
|
||||
WHERE PORTFOLIO_IDNTF_NO = #{portfolioIdntfNo}
|
||||
AND DEL_YN = 'N'
|
||||
""")
|
||||
int incrementViewCount(@Param("portfolioIdntfNo") String portfolioIdntfNo);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM TN_PORTFOLIO
|
||||
WHERE DEL_YN = 'N'
|
||||
""")
|
||||
int countPortfolio();
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package kr.iotdoor.boot.mapper;
|
||||
|
||||
import kr.iotdoor.boot.model.user.UserAccount;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
USER_IDNTF_NO,
|
||||
USER_ID,
|
||||
PSWD,
|
||||
USER_NM,
|
||||
EML_ADDR,
|
||||
USER_STTS_CD,
|
||||
USER_TYPE_CD,
|
||||
JOIN_DT,
|
||||
INDVDL_INFO_AGRE_DT,
|
||||
LOCK_YN,
|
||||
LGN_FAIL_NMTM,
|
||||
LOCK_DT,
|
||||
LAST_LGN_DT
|
||||
FROM TN_USER
|
||||
WHERE USER_ID = #{userId}
|
||||
""")
|
||||
UserAccount selectByUserId(@Param("userId") String userId);
|
||||
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM TN_USER
|
||||
WHERE USER_ID = #{userId}
|
||||
""")
|
||||
int countByUserId(@Param("userId") String userId);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO TN_USER (
|
||||
USER_IDNTF_NO,
|
||||
USER_ID,
|
||||
PSWD,
|
||||
USER_NM,
|
||||
EML_ADDR,
|
||||
USER_STTS_CD,
|
||||
USER_TYPE_CD,
|
||||
JOIN_DT,
|
||||
INDVDL_INFO_AGRE_DT,
|
||||
LOCK_YN,
|
||||
LGN_FAIL_NMTM,
|
||||
LOCK_DT,
|
||||
LAST_LGN_DT
|
||||
) VALUES (
|
||||
#{userIdntfNo},
|
||||
#{userId},
|
||||
#{pswd},
|
||||
#{userNm},
|
||||
#{emlAddr},
|
||||
#{userSttsCd},
|
||||
#{userTypeCd},
|
||||
#{joinDt},
|
||||
#{indvdlInfoAgreDt},
|
||||
#{lockYn},
|
||||
#{lgnFailNmtm},
|
||||
#{lockDt},
|
||||
#{lastLgnDt}
|
||||
)
|
||||
""")
|
||||
int insertUser(UserAccount userAccount);
|
||||
|
||||
@Update("""
|
||||
UPDATE TN_USER
|
||||
SET LAST_LGN_DT = CURRENT_TIMESTAMP
|
||||
WHERE USER_IDNTF_NO = #{userIdntfNo}
|
||||
""")
|
||||
int updateLastLoginDt(@Param("userIdntfNo") String userIdntfNo);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package kr.iotdoor.boot.model.admin;
|
||||
|
||||
public record MigrationFeature(
|
||||
String name,
|
||||
String status,
|
||||
String summary,
|
||||
String routes
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.iotdoor.boot.model.contact;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public record ContactRequest(
|
||||
@NotBlank(message = "이름은 필수입니다.")
|
||||
@Size(max = 60, message = "이름은 60자 이하여야 합니다.")
|
||||
String name,
|
||||
|
||||
@NotBlank(message = "이메일은 필수입니다.")
|
||||
@Email(message = "올바른 이메일 형식이 아닙니다.")
|
||||
@Size(max = 120, message = "이메일은 120자 이하여야 합니다.")
|
||||
String email,
|
||||
|
||||
@NotBlank(message = "연락처는 필수입니다.")
|
||||
@Pattern(regexp = "^01[016789]-?\\d{3,4}-?\\d{4}$", message = "올바른 휴대폰번호 형식이 아닙니다.")
|
||||
String phone,
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다.")
|
||||
@Size(max = 120, message = "제목은 120자 이하여야 합니다.")
|
||||
String subject,
|
||||
|
||||
@NotBlank(message = "문의 내용은 필수입니다.")
|
||||
@Size(max = 3000, message = "문의 내용은 3000자 이하여야 합니다.")
|
||||
String message
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package kr.iotdoor.boot.model.contact;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class ContactSubmission {
|
||||
|
||||
private String contactIdntfNo;
|
||||
private String name;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String subject;
|
||||
private String message;
|
||||
private LocalDateTime submittedAt;
|
||||
|
||||
public String getContactIdntfNo() {
|
||||
return contactIdntfNo;
|
||||
}
|
||||
|
||||
public void setContactIdntfNo(String contactIdntfNo) {
|
||||
this.contactIdntfNo = contactIdntfNo;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public LocalDateTime getSubmittedAt() {
|
||||
return submittedAt;
|
||||
}
|
||||
|
||||
public void setSubmittedAt(LocalDateTime submittedAt) {
|
||||
this.submittedAt = submittedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package kr.iotdoor.boot.model.file;
|
||||
|
||||
public class StoredFile {
|
||||
|
||||
private String atchFileIdntfNo;
|
||||
private int fileSn;
|
||||
private String fileStrgPathNm;
|
||||
private String fileStrgNm;
|
||||
private String orgnlFileNm;
|
||||
private String fileExtnNm;
|
||||
private String fileCn;
|
||||
private long fileSz;
|
||||
private String url;
|
||||
|
||||
public String getAtchFileIdntfNo() {
|
||||
return atchFileIdntfNo;
|
||||
}
|
||||
|
||||
public void setAtchFileIdntfNo(String atchFileIdntfNo) {
|
||||
this.atchFileIdntfNo = atchFileIdntfNo;
|
||||
}
|
||||
|
||||
public int getFileSn() {
|
||||
return fileSn;
|
||||
}
|
||||
|
||||
public void setFileSn(int fileSn) {
|
||||
this.fileSn = fileSn;
|
||||
}
|
||||
|
||||
public String getFileStrgPathNm() {
|
||||
return fileStrgPathNm;
|
||||
}
|
||||
|
||||
public void setFileStrgPathNm(String fileStrgPathNm) {
|
||||
this.fileStrgPathNm = fileStrgPathNm;
|
||||
}
|
||||
|
||||
public String getFileStrgNm() {
|
||||
return fileStrgNm;
|
||||
}
|
||||
|
||||
public void setFileStrgNm(String fileStrgNm) {
|
||||
this.fileStrgNm = fileStrgNm;
|
||||
}
|
||||
|
||||
public String getOrgnlFileNm() {
|
||||
return orgnlFileNm;
|
||||
}
|
||||
|
||||
public void setOrgnlFileNm(String orgnlFileNm) {
|
||||
this.orgnlFileNm = orgnlFileNm;
|
||||
}
|
||||
|
||||
public String getFileExtnNm() {
|
||||
return fileExtnNm;
|
||||
}
|
||||
|
||||
public void setFileExtnNm(String fileExtnNm) {
|
||||
this.fileExtnNm = fileExtnNm;
|
||||
}
|
||||
|
||||
public String getFileCn() {
|
||||
return fileCn;
|
||||
}
|
||||
|
||||
public void setFileCn(String fileCn) {
|
||||
this.fileCn = fileCn;
|
||||
}
|
||||
|
||||
public long getFileSz() {
|
||||
return fileSz;
|
||||
}
|
||||
|
||||
public void setFileSz(long fileSz) {
|
||||
this.fileSz = fileSz;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package kr.iotdoor.boot.model.portfolio;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
public class PortfolioForm {
|
||||
|
||||
private String portfolioIdntfNo;
|
||||
|
||||
@NotBlank(message = "제목은 필수입니다.")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "내용은 필수입니다.")
|
||||
private String content;
|
||||
|
||||
private String thumbnailAtchFileIdntfNo;
|
||||
|
||||
public String getPortfolioIdntfNo() {
|
||||
return portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public void setPortfolioIdntfNo(String portfolioIdntfNo) {
|
||||
this.portfolioIdntfNo = portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getThumbnailAtchFileIdntfNo() {
|
||||
return thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
|
||||
public void setThumbnailAtchFileIdntfNo(String thumbnailAtchFileIdntfNo) {
|
||||
this.thumbnailAtchFileIdntfNo = thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package kr.iotdoor.boot.model.portfolio;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class PortfolioItem {
|
||||
|
||||
private String portfolioIdntfNo;
|
||||
private String title;
|
||||
private String summary;
|
||||
private String imageUrl;
|
||||
private String content;
|
||||
private String thumbnailAtchFileIdntfNo;
|
||||
private int thumbnailFileSn;
|
||||
private LocalDateTime registeredAt;
|
||||
private long viewCount;
|
||||
private String registeredByUserIdntfNo;
|
||||
private String registeredByName;
|
||||
private String delYn;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public String getPortfolioIdntfNo() {
|
||||
return portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public void setPortfolioIdntfNo(String portfolioIdntfNo) {
|
||||
this.portfolioIdntfNo = portfolioIdntfNo;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public void setSummary(String summary) {
|
||||
this.summary = summary;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getThumbnailAtchFileIdntfNo() {
|
||||
return thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
|
||||
public void setThumbnailAtchFileIdntfNo(String thumbnailAtchFileIdntfNo) {
|
||||
this.thumbnailAtchFileIdntfNo = thumbnailAtchFileIdntfNo;
|
||||
}
|
||||
|
||||
public int getThumbnailFileSn() {
|
||||
return thumbnailFileSn;
|
||||
}
|
||||
|
||||
public void setThumbnailFileSn(int thumbnailFileSn) {
|
||||
this.thumbnailFileSn = thumbnailFileSn;
|
||||
}
|
||||
|
||||
public LocalDateTime getRegisteredAt() {
|
||||
return registeredAt;
|
||||
}
|
||||
|
||||
public void setRegisteredAt(LocalDateTime registeredAt) {
|
||||
this.registeredAt = registeredAt;
|
||||
}
|
||||
|
||||
public long getViewCount() {
|
||||
return viewCount;
|
||||
}
|
||||
|
||||
public void setViewCount(long viewCount) {
|
||||
this.viewCount = viewCount;
|
||||
}
|
||||
|
||||
public String getRegisteredByUserIdntfNo() {
|
||||
return registeredByUserIdntfNo;
|
||||
}
|
||||
|
||||
public void setRegisteredByUserIdntfNo(String registeredByUserIdntfNo) {
|
||||
this.registeredByUserIdntfNo = registeredByUserIdntfNo;
|
||||
}
|
||||
|
||||
public String getRegisteredByName() {
|
||||
return registeredByName;
|
||||
}
|
||||
|
||||
public void setRegisteredByName(String registeredByName) {
|
||||
this.registeredByName = registeredByName;
|
||||
}
|
||||
|
||||
public String getDelYn() {
|
||||
return delYn;
|
||||
}
|
||||
|
||||
public void setDelYn(String delYn) {
|
||||
this.delYn = delYn;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package kr.iotdoor.boot.model.user;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class AdminUserPrincipal implements UserDetails {
|
||||
|
||||
private final UserAccount userAccount;
|
||||
|
||||
public AdminUserPrincipal(UserAccount userAccount) {
|
||||
this.userAccount = userAccount;
|
||||
}
|
||||
|
||||
public String getUserIdntfNo() {
|
||||
return userAccount.getUserIdntfNo();
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return userAccount.getUserNm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return userAccount.getPswd();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return userAccount.getUserId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountNonLocked() {
|
||||
return !"Y".equalsIgnoreCase(userAccount.getLockYn());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return "A".equalsIgnoreCase(userAccount.getUserSttsCd()) || "P".equalsIgnoreCase(userAccount.getUserSttsCd());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package kr.iotdoor.boot.model.user;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class UserAccount {
|
||||
|
||||
private String userIdntfNo;
|
||||
private String userId;
|
||||
private String pswd;
|
||||
private String userNm;
|
||||
private String emlAddr;
|
||||
private String userSttsCd;
|
||||
private String userTypeCd;
|
||||
private LocalDateTime joinDt;
|
||||
private LocalDateTime indvdlInfoAgreDt;
|
||||
private String lockYn;
|
||||
private Integer lgnFailNmtm;
|
||||
private LocalDateTime lockDt;
|
||||
private LocalDateTime lastLgnDt;
|
||||
|
||||
public String getUserIdntfNo() {
|
||||
return userIdntfNo;
|
||||
}
|
||||
|
||||
public void setUserIdntfNo(String userIdntfNo) {
|
||||
this.userIdntfNo = userIdntfNo;
|
||||
}
|
||||
|
||||
public String getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getPswd() {
|
||||
return pswd;
|
||||
}
|
||||
|
||||
public void setPswd(String pswd) {
|
||||
this.pswd = pswd;
|
||||
}
|
||||
|
||||
public String getUserNm() {
|
||||
return userNm;
|
||||
}
|
||||
|
||||
public void setUserNm(String userNm) {
|
||||
this.userNm = userNm;
|
||||
}
|
||||
|
||||
public String getEmlAddr() {
|
||||
return emlAddr;
|
||||
}
|
||||
|
||||
public void setEmlAddr(String emlAddr) {
|
||||
this.emlAddr = emlAddr;
|
||||
}
|
||||
|
||||
public String getUserSttsCd() {
|
||||
return userSttsCd;
|
||||
}
|
||||
|
||||
public void setUserSttsCd(String userSttsCd) {
|
||||
this.userSttsCd = userSttsCd;
|
||||
}
|
||||
|
||||
public String getUserTypeCd() {
|
||||
return userTypeCd;
|
||||
}
|
||||
|
||||
public void setUserTypeCd(String userTypeCd) {
|
||||
this.userTypeCd = userTypeCd;
|
||||
}
|
||||
|
||||
public LocalDateTime getJoinDt() {
|
||||
return joinDt;
|
||||
}
|
||||
|
||||
public void setJoinDt(LocalDateTime joinDt) {
|
||||
this.joinDt = joinDt;
|
||||
}
|
||||
|
||||
public LocalDateTime getIndvdlInfoAgreDt() {
|
||||
return indvdlInfoAgreDt;
|
||||
}
|
||||
|
||||
public void setIndvdlInfoAgreDt(LocalDateTime indvdlInfoAgreDt) {
|
||||
this.indvdlInfoAgreDt = indvdlInfoAgreDt;
|
||||
}
|
||||
|
||||
public String getLockYn() {
|
||||
return lockYn;
|
||||
}
|
||||
|
||||
public void setLockYn(String lockYn) {
|
||||
this.lockYn = lockYn;
|
||||
}
|
||||
|
||||
public Integer getLgnFailNmtm() {
|
||||
return lgnFailNmtm;
|
||||
}
|
||||
|
||||
public void setLgnFailNmtm(Integer lgnFailNmtm) {
|
||||
this.lgnFailNmtm = lgnFailNmtm;
|
||||
}
|
||||
|
||||
public LocalDateTime getLockDt() {
|
||||
return lockDt;
|
||||
}
|
||||
|
||||
public void setLockDt(LocalDateTime lockDt) {
|
||||
this.lockDt = lockDt;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastLgnDt() {
|
||||
return lastLgnDt;
|
||||
}
|
||||
|
||||
public void setLastLgnDt(LocalDateTime lastLgnDt) {
|
||||
this.lastLgnDt = lastLgnDt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.config.IotdoorProperties;
|
||||
import kr.iotdoor.boot.mapper.UserMapper;
|
||||
import kr.iotdoor.boot.model.user.UserAccount;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Configuration
|
||||
public class AdminAccountInitializer {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AdminAccountInitializer.class);
|
||||
|
||||
@Bean
|
||||
public ApplicationRunner adminUserSeedRunner(
|
||||
UserMapper userMapper,
|
||||
JdbcTemplate jdbcTemplate,
|
||||
IotdoorProperties properties,
|
||||
PasswordEncoder passwordEncoder
|
||||
) {
|
||||
return args -> {
|
||||
String adminUsername = properties.getSecurity().getAdminUsername();
|
||||
if (userMapper.countByUserId(adminUsername) > 0) {
|
||||
UserAccount existing = userMapper.selectByUserId(adminUsername);
|
||||
if (existing != null && !isBcryptHash(existing.getPswd())) {
|
||||
jdbcTemplate.update("""
|
||||
UPDATE TN_USER
|
||||
SET
|
||||
PSWD = ?,
|
||||
USER_NM = ?,
|
||||
USER_STTS_CD = ?,
|
||||
USER_TYPE_CD = ?,
|
||||
LOCK_YN = ?,
|
||||
LGN_FAIL_NMTM = 0,
|
||||
LOCK_DT = NULL
|
||||
WHERE USER_ID = ?
|
||||
""",
|
||||
passwordEncoder.encode(properties.getSecurity().getAdminPassword()),
|
||||
properties.getSecurity().getAdminDisplayName(),
|
||||
"A",
|
||||
"ADM",
|
||||
"N",
|
||||
adminUsername
|
||||
);
|
||||
LOGGER.info("Normalized legacy admin account for Spring Security login. username={}", adminUsername);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
UserAccount userAccount = new UserAccount();
|
||||
userAccount.setUserIdntfNo(UUID.randomUUID().toString().replace("-", ""));
|
||||
userAccount.setUserId(adminUsername);
|
||||
userAccount.setPswd(passwordEncoder.encode(properties.getSecurity().getAdminPassword()));
|
||||
userAccount.setUserNm(properties.getSecurity().getAdminDisplayName());
|
||||
userAccount.setEmlAddr("admin@iotdoor.local");
|
||||
userAccount.setUserSttsCd("A");
|
||||
userAccount.setUserTypeCd("ADM");
|
||||
userAccount.setJoinDt(LocalDateTime.now());
|
||||
userAccount.setIndvdlInfoAgreDt(LocalDateTime.now());
|
||||
userAccount.setLockYn("N");
|
||||
userAccount.setLgnFailNmtm(0);
|
||||
|
||||
if (hasLegacyColumns(jdbcTemplate)) {
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO TN_USER (
|
||||
USER_IDNTF_NO,
|
||||
GROUP_IDNTF_NO,
|
||||
AUTHRT_CD,
|
||||
USER_ID,
|
||||
PSWD,
|
||||
USER_NM,
|
||||
EML_ADDR,
|
||||
USER_STTS_CD,
|
||||
USER_TYPE_CD,
|
||||
JOIN_DT,
|
||||
INDVDL_INFO_AGRE_DT,
|
||||
LOCK_YN,
|
||||
LGN_FAIL_NMTM,
|
||||
LOCK_DT,
|
||||
LAST_LGN_DT
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
userAccount.getUserIdntfNo(),
|
||||
"group-boot-default",
|
||||
"ROLE_ADMIN",
|
||||
userAccount.getUserId(),
|
||||
userAccount.getPswd(),
|
||||
userAccount.getUserNm(),
|
||||
userAccount.getEmlAddr(),
|
||||
userAccount.getUserSttsCd(),
|
||||
userAccount.getUserTypeCd(),
|
||||
userAccount.getJoinDt(),
|
||||
userAccount.getIndvdlInfoAgreDt(),
|
||||
userAccount.getLockYn(),
|
||||
userAccount.getLgnFailNmtm(),
|
||||
userAccount.getLockDt(),
|
||||
userAccount.getLastLgnDt()
|
||||
);
|
||||
} else {
|
||||
userMapper.insertUser(userAccount);
|
||||
}
|
||||
|
||||
LOGGER.info("Seeded default admin account into TN_USER. username={}", adminUsername);
|
||||
};
|
||||
}
|
||||
|
||||
private boolean hasLegacyColumns(JdbcTemplate jdbcTemplate) {
|
||||
Integer count = jdbcTemplate.queryForObject("""
|
||||
SELECT COUNT(*)
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'TN_USER'
|
||||
AND COLUMN_NAME IN ('GROUP_IDNTF_NO', 'AUTHRT_CD')
|
||||
""", Integer.class);
|
||||
return count != null && count == 2;
|
||||
}
|
||||
|
||||
private boolean isBcryptHash(String value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
return value.startsWith("$2a$") || value.startsWith("$2b$") || value.startsWith("$2y$");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.UserMapper;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import kr.iotdoor.boot.model.user.UserAccount;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AdminUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public AdminUserDetailsService(UserMapper userMapper) {
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserAccount userAccount = userMapper.selectByUserId(username);
|
||||
if (userAccount == null) {
|
||||
throw new UsernameNotFoundException("관리자 계정을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
return new AdminUserPrincipal(userAccount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.ContactMapper;
|
||||
import kr.iotdoor.boot.model.contact.ContactRequest;
|
||||
import kr.iotdoor.boot.model.contact.ContactSubmission;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ContactSubmissionService {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ContactSubmissionService.class);
|
||||
|
||||
private final ContactMapper contactMapper;
|
||||
|
||||
public ContactSubmissionService(ContactMapper contactMapper) {
|
||||
this.contactMapper = contactMapper;
|
||||
}
|
||||
|
||||
public ContactSubmission submit(ContactRequest request) {
|
||||
ContactSubmission submission = new ContactSubmission();
|
||||
submission.setContactIdntfNo(UUID.randomUUID().toString().replace("-", ""));
|
||||
submission.setName(request.name().trim());
|
||||
submission.setEmail(request.email().trim());
|
||||
submission.setPhone(request.phone().trim());
|
||||
submission.setSubject(request.subject().trim());
|
||||
submission.setMessage(request.message().trim());
|
||||
submission.setSubmittedAt(LocalDateTime.now());
|
||||
|
||||
contactMapper.insertContact(submission);
|
||||
|
||||
LOGGER.info("Contact inquiry received from {} <{}> / subject={}",
|
||||
submission.getName(), submission.getEmail(), submission.getSubject());
|
||||
|
||||
return submission;
|
||||
}
|
||||
|
||||
public List<ContactSubmission> recentSubmissions(int limit) {
|
||||
return contactMapper.selectRecentContacts(limit);
|
||||
}
|
||||
|
||||
public List<ContactSubmission> findAll() {
|
||||
return contactMapper.selectAllContacts();
|
||||
}
|
||||
|
||||
public int count() {
|
||||
return contactMapper.selectContactCount();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.config.IotdoorProperties;
|
||||
import kr.iotdoor.boot.mapper.AtchFileDetailMapper;
|
||||
import kr.iotdoor.boot.mapper.AtchFileMapper;
|
||||
import kr.iotdoor.boot.model.file.StoredFile;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class FileStorageService {
|
||||
|
||||
private static final DateTimeFormatter DATE_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd");
|
||||
|
||||
private final IotdoorProperties properties;
|
||||
private final AtchFileMapper atchFileMapper;
|
||||
private final AtchFileDetailMapper atchFileDetailMapper;
|
||||
|
||||
public FileStorageService(
|
||||
IotdoorProperties properties,
|
||||
AtchFileMapper atchFileMapper,
|
||||
AtchFileDetailMapper atchFileDetailMapper
|
||||
) {
|
||||
this.properties = properties;
|
||||
this.atchFileMapper = atchFileMapper;
|
||||
this.atchFileDetailMapper = atchFileDetailMapper;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public StoredFile storeSingleFile(MultipartFile multipartFile, String directory, String description) {
|
||||
if (multipartFile == null || multipartFile.isEmpty()) {
|
||||
throw new IllegalArgumentException("업로드할 파일이 없습니다.");
|
||||
}
|
||||
|
||||
String atchFileIdntfNo = UUID.randomUUID().toString().replace("-", "");
|
||||
atchFileMapper.insertAtchFile(atchFileIdntfNo);
|
||||
|
||||
String sanitizedDirectory = sanitizeDirectory(directory);
|
||||
String relativeDirectory = sanitizedDirectory + "/" + LocalDate.now().format(DATE_PATH_FORMATTER);
|
||||
Path rootPath = Paths.get(properties.getFile().getUploadPath()).toAbsolutePath().normalize();
|
||||
Path storageDirectory = rootPath.resolve(relativeDirectory).normalize();
|
||||
|
||||
if (!storageDirectory.startsWith(rootPath)) {
|
||||
throw new IllegalArgumentException("허용되지 않은 업로드 경로입니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
Files.createDirectories(storageDirectory);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("업로드 디렉터리를 생성할 수 없습니다.", exception);
|
||||
}
|
||||
|
||||
String originalFileName = StringUtils.cleanPath(multipartFile.getOriginalFilename());
|
||||
String extension = extractExtension(originalFileName);
|
||||
String storedFileName = UUID.randomUUID().toString().replace("-", "");
|
||||
if (StringUtils.hasText(extension)) {
|
||||
storedFileName += "." + extension;
|
||||
}
|
||||
|
||||
Path targetPath = storageDirectory.resolve(storedFileName).normalize();
|
||||
if (!targetPath.startsWith(rootPath)) {
|
||||
throw new IllegalArgumentException("허용되지 않은 파일 저장 위치입니다.");
|
||||
}
|
||||
|
||||
try {
|
||||
multipartFile.transferTo(targetPath);
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("파일 저장에 실패했습니다.", exception);
|
||||
}
|
||||
|
||||
StoredFile storedFile = new StoredFile();
|
||||
storedFile.setAtchFileIdntfNo(atchFileIdntfNo);
|
||||
storedFile.setFileSn(atchFileDetailMapper.selectAtchFileDtlNextSn(atchFileIdntfNo));
|
||||
storedFile.setFileStrgPathNm(relativeDirectory.replace('\\', '/'));
|
||||
storedFile.setFileStrgNm(storedFileName);
|
||||
storedFile.setOrgnlFileNm(originalFileName);
|
||||
storedFile.setFileExtnNm(extension);
|
||||
storedFile.setFileCn(description);
|
||||
storedFile.setFileSz(multipartFile.getSize());
|
||||
atchFileDetailMapper.insertAtchFileDtl(storedFile);
|
||||
storedFile.setUrl(toPublicUrl(atchFileIdntfNo, storedFile.getFileSn()));
|
||||
return storedFile;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public StoredFile getStoredFile(String atchFileIdntfNo, int fileSn) {
|
||||
StoredFile storedFile = atchFileDetailMapper.selectAtchFileDtl(atchFileIdntfNo, fileSn);
|
||||
if (storedFile != null) {
|
||||
storedFile.setUrl(toPublicUrl(atchFileIdntfNo, fileSn));
|
||||
}
|
||||
return storedFile;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Path resolvePath(StoredFile storedFile) {
|
||||
Path rootPath = Paths.get(properties.getFile().getUploadPath()).toAbsolutePath().normalize();
|
||||
Path targetPath = rootPath
|
||||
.resolve(storedFile.getFileStrgPathNm())
|
||||
.resolve(storedFile.getFileStrgNm())
|
||||
.normalize();
|
||||
|
||||
if (!targetPath.startsWith(rootPath)) {
|
||||
throw new IllegalArgumentException("허용되지 않은 파일 경로입니다.");
|
||||
}
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
public String toPublicUrl(String atchFileIdntfNo, int fileSn) {
|
||||
return "/comn/file/view/" + atchFileIdntfNo + "/" + fileSn;
|
||||
}
|
||||
|
||||
private String sanitizeDirectory(String directory) {
|
||||
String value = StringUtils.hasText(directory) ? directory : "common";
|
||||
value = value.replace('\\', '/').replaceAll("[^a-zA-Z0-9/_-]", "");
|
||||
value = value.replaceAll("/+", "/");
|
||||
value = value.replaceAll("^/+", "");
|
||||
return StringUtils.hasText(value) ? value : "common";
|
||||
}
|
||||
|
||||
private String extractExtension(String fileName) {
|
||||
if (!StringUtils.hasText(fileName) || !fileName.contains(".")) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.UserMapper;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class LoginAuditService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public LoginAuditService(UserMapper userMapper) {
|
||||
this.userMapper = userMapper;
|
||||
}
|
||||
|
||||
@EventListener
|
||||
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
|
||||
Object principal = event.getAuthentication().getPrincipal();
|
||||
if (principal instanceof AdminUserPrincipal adminUserPrincipal) {
|
||||
userMapper.updateLastLoginDt(adminUserPrincipal.getUserIdntfNo());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.model.admin.MigrationFeature;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class MigrationCatalogService {
|
||||
|
||||
public List<MigrationFeature> getFeatures() {
|
||||
return List.of(
|
||||
new MigrationFeature(
|
||||
"공개 메인/정적 페이지",
|
||||
"운영 가능",
|
||||
"메인, 연락처, 포트폴리오와 공용 레이아웃을 Boot 3 템플릿으로 재배치했습니다.",
|
||||
"/, /index.do, /contact.do, /portfolio.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"관리자 인증",
|
||||
"DB 연동 완료",
|
||||
"TN_USER 기반으로 관리자 로그인/로그아웃과 BCrypt 비밀번호 검증을 연결했습니다.",
|
||||
"/comn/user/login/loginUser.do, /comn/user/login/actionLogin.do, /mfuc/index.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"문의 접수",
|
||||
"DB 저장 완료",
|
||||
"문의 API가 TN_CONTACT_INQRY 테이블에 저장되고 관리자 화면에서 최근 문의를 확인할 수 있습니다.",
|
||||
"/api/contact, /mfuc/contact/list.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"포트폴리오",
|
||||
"DB 연동 완료",
|
||||
"TN_PORTFOLIO와 첨부파일 모듈을 추가해 공개 목록/상세와 관리자 등록/수정/삭제를 구성했습니다.",
|
||||
"/portfolio.do, /portfolio/{id}, /mfuc/portfolio/list.do"
|
||||
),
|
||||
new MigrationFeature(
|
||||
"파일 모듈",
|
||||
"기초 모듈 완료",
|
||||
"레거시 TN_ATCH_FILE / TN_ATCH_FILE_DTL 스키마를 따라 업로드 및 파일 조회 엔드포인트를 구성했습니다.",
|
||||
"/mfuc/file/upload.do, /comn/file/view/**"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package kr.iotdoor.boot.service;
|
||||
|
||||
import kr.iotdoor.boot.mapper.PortfolioMapper;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioForm;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioItem;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class PortfolioService {
|
||||
|
||||
private static final String FALLBACK_IMAGE = "/resources/kr/iotdoor/comn/site/layout/image/common/no_img.svg";
|
||||
|
||||
private final PortfolioMapper portfolioMapper;
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public PortfolioService(PortfolioMapper portfolioMapper, FileStorageService fileStorageService) {
|
||||
this.portfolioMapper = portfolioMapper;
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PortfolioItem> getPortfolioItems() {
|
||||
return portfolioMapper.selectPortfolioList().stream()
|
||||
.map(this::enrich)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PortfolioItem getPortfolioDetail(String portfolioIdntfNo) {
|
||||
if (portfolioMapper.incrementViewCount(portfolioIdntfNo) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
PortfolioItem item = portfolioMapper.selectPortfolioDetail(portfolioIdntfNo);
|
||||
return item == null ? null : enrich(item);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<PortfolioItem> getAdminPortfolioItems() {
|
||||
return portfolioMapper.selectPortfolioList().stream()
|
||||
.map(this::enrich)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PortfolioItem getPortfolioForEdit(String portfolioIdntfNo) {
|
||||
PortfolioItem item = portfolioMapper.selectPortfolioDetail(portfolioIdntfNo);
|
||||
return item == null ? null : enrich(item);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public int count() {
|
||||
return portfolioMapper.countPortfolio();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void save(PortfolioForm form, MultipartFile thumbnailFile, AdminUserPrincipal principal) {
|
||||
String thumbnailAtchFileIdntfNo = form.getThumbnailAtchFileIdntfNo();
|
||||
int thumbnailFileSn = 1;
|
||||
|
||||
if (thumbnailFile != null && !thumbnailFile.isEmpty()) {
|
||||
thumbnailAtchFileIdntfNo = fileStorageService.storeSingleFile(
|
||||
thumbnailFile,
|
||||
"portfolio/thumbnail",
|
||||
"portfolio-thumbnail"
|
||||
).getAtchFileIdntfNo();
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(thumbnailAtchFileIdntfNo)) {
|
||||
throw new IllegalArgumentException("대표 이미지는 필수입니다.");
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(form.getPortfolioIdntfNo())) {
|
||||
PortfolioItem existing = portfolioMapper.selectPortfolioDetail(form.getPortfolioIdntfNo());
|
||||
if (existing == null) {
|
||||
throw new IllegalArgumentException("수정할 포트폴리오를 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
existing.setTitle(form.getTitle().trim());
|
||||
existing.setContent(form.getContent().trim());
|
||||
existing.setThumbnailAtchFileIdntfNo(thumbnailAtchFileIdntfNo);
|
||||
existing.setThumbnailFileSn(thumbnailFileSn);
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
portfolioMapper.updatePortfolio(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
PortfolioItem item = new PortfolioItem();
|
||||
item.setPortfolioIdntfNo(newIdentifier());
|
||||
item.setTitle(form.getTitle().trim());
|
||||
item.setContent(form.getContent().trim());
|
||||
item.setThumbnailAtchFileIdntfNo(thumbnailAtchFileIdntfNo);
|
||||
item.setThumbnailFileSn(thumbnailFileSn);
|
||||
item.setRegisteredAt(LocalDateTime.now());
|
||||
item.setRegisteredByUserIdntfNo(principal.getUserIdntfNo());
|
||||
portfolioMapper.insertPortfolio(item);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(String portfolioIdntfNo) {
|
||||
portfolioMapper.deletePortfolio(portfolioIdntfNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public String resolveThumbnailUrl(String atchFileIdntfNo, int fileSn) {
|
||||
if (!StringUtils.hasText(atchFileIdntfNo)) {
|
||||
return FALLBACK_IMAGE;
|
||||
}
|
||||
return fileStorageService.toPublicUrl(atchFileIdntfNo, fileSn <= 0 ? 1 : fileSn);
|
||||
}
|
||||
|
||||
private PortfolioItem enrich(PortfolioItem item) {
|
||||
item.setSummary(summarize(item.getContent()));
|
||||
item.setImageUrl(resolveThumbnailUrl(item.getThumbnailAtchFileIdntfNo(), item.getThumbnailFileSn()));
|
||||
return item;
|
||||
}
|
||||
|
||||
private String summarize(String content) {
|
||||
if (!StringUtils.hasText(content)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String plainText = content
|
||||
.replaceAll("<[^>]*>", " ")
|
||||
.replace(" ", " ")
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
|
||||
if (plainText.length() <= 120) {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
return plainText.substring(0, 120) + "...";
|
||||
}
|
||||
|
||||
private String newIdentifier() {
|
||||
return UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.service.ContactSubmissionService;
|
||||
import kr.iotdoor.boot.service.MigrationCatalogService;
|
||||
import kr.iotdoor.boot.service.PortfolioService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class AdminController {
|
||||
|
||||
private final ContactSubmissionService contactSubmissionService;
|
||||
private final MigrationCatalogService migrationCatalogService;
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public AdminController(
|
||||
ContactSubmissionService contactSubmissionService,
|
||||
MigrationCatalogService migrationCatalogService,
|
||||
PortfolioService portfolioService
|
||||
) {
|
||||
this.contactSubmissionService = contactSubmissionService;
|
||||
this.migrationCatalogService = migrationCatalogService;
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping({"/mfuc/index.do", "/mngr/index.do"})
|
||||
public String dashboard(Model model) {
|
||||
model.addAttribute("recentContacts", contactSubmissionService.recentSubmissions(10));
|
||||
model.addAttribute("contactCount", contactSubmissionService.count());
|
||||
model.addAttribute("portfolioCount", portfolioService.count());
|
||||
model.addAttribute("migrationFeatures", migrationCatalogService.getFeatures());
|
||||
return "kr/iotdoor/boot/admin/dashboard";
|
||||
}
|
||||
|
||||
@GetMapping("/mfuc/contact/list.do")
|
||||
public String contactList(Model model) {
|
||||
model.addAttribute("contacts", contactSubmissionService.findAll());
|
||||
return "kr/iotdoor/boot/admin/contact/list";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioForm;
|
||||
import kr.iotdoor.boot.model.portfolio.PortfolioItem;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import kr.iotdoor.boot.service.PortfolioService;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Controller
|
||||
public class AdminPortfolioController {
|
||||
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public AdminPortfolioController(PortfolioService portfolioService) {
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping("/mfuc/portfolio/list.do")
|
||||
public String portfolioList(Model model) {
|
||||
model.addAttribute("portfolioItems", portfolioService.getAdminPortfolioItems());
|
||||
return "kr/iotdoor/boot/admin/portfolio/list";
|
||||
}
|
||||
|
||||
@GetMapping("/mfuc/portfolio/form.do")
|
||||
public String portfolioForm(
|
||||
@RequestParam(name = "portfolioIdntfNo", required = false) String portfolioIdntfNo,
|
||||
Model model
|
||||
) {
|
||||
PortfolioForm form = new PortfolioForm();
|
||||
if (portfolioIdntfNo != null && !portfolioIdntfNo.isBlank()) {
|
||||
PortfolioItem portfolio = portfolioService.getPortfolioForEdit(portfolioIdntfNo);
|
||||
if (portfolio != null) {
|
||||
form.setPortfolioIdntfNo(portfolio.getPortfolioIdntfNo());
|
||||
form.setTitle(portfolio.getTitle());
|
||||
form.setContent(portfolio.getContent());
|
||||
form.setThumbnailAtchFileIdntfNo(portfolio.getThumbnailAtchFileIdntfNo());
|
||||
model.addAttribute("currentThumbnailUrl", portfolio.getImageUrl());
|
||||
model.addAttribute("portfolioMeta", portfolio);
|
||||
}
|
||||
}
|
||||
|
||||
model.addAttribute("form", form);
|
||||
return "kr/iotdoor/boot/admin/portfolio/form";
|
||||
}
|
||||
|
||||
@PostMapping("/mfuc/portfolio/save.do")
|
||||
public String savePortfolio(
|
||||
@Valid @ModelAttribute("form") PortfolioForm form,
|
||||
BindingResult bindingResult,
|
||||
@RequestParam(name = "thumbnailFile", required = false) MultipartFile thumbnailFile,
|
||||
Authentication authentication,
|
||||
Model model
|
||||
) {
|
||||
if ((thumbnailFile == null || thumbnailFile.isEmpty()) && (form.getThumbnailAtchFileIdntfNo() == null || form.getThumbnailAtchFileIdntfNo().isBlank())) {
|
||||
bindingResult.rejectValue("thumbnailAtchFileIdntfNo", "required", "대표 이미지는 필수입니다.");
|
||||
}
|
||||
|
||||
if (bindingResult.hasErrors()) {
|
||||
if (form.getThumbnailAtchFileIdntfNo() != null && !form.getThumbnailAtchFileIdntfNo().isBlank()) {
|
||||
model.addAttribute("currentThumbnailUrl", portfolioService.resolveThumbnailUrl(form.getThumbnailAtchFileIdntfNo(), 1));
|
||||
}
|
||||
return "kr/iotdoor/boot/admin/portfolio/form";
|
||||
}
|
||||
|
||||
portfolioService.save(form, thumbnailFile, (AdminUserPrincipal) authentication.getPrincipal());
|
||||
return "redirect:/mfuc/portfolio/list.do?saved=1";
|
||||
}
|
||||
|
||||
@PostMapping("/mfuc/portfolio/delete.do")
|
||||
public String deletePortfolio(@RequestParam("portfolioIdntfNo") String portfolioIdntfNo) {
|
||||
portfolioService.delete(portfolioIdntfNo);
|
||||
return "redirect:/mfuc/portfolio/list.do?deleted=1";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class AuthController {
|
||||
|
||||
@GetMapping("/comn/user/login/login.do")
|
||||
public String loginRedirect() {
|
||||
return "redirect:/comn/user/login/loginUser.do";
|
||||
}
|
||||
|
||||
@GetMapping("/comn/user/login/loginUser.do")
|
||||
public String loginPage(
|
||||
@RequestParam(name = "login_error", required = false) String loginError,
|
||||
Authentication authentication,
|
||||
HttpServletRequest request,
|
||||
Model model
|
||||
) {
|
||||
if (authentication != null
|
||||
&& authentication.isAuthenticated()
|
||||
&& !(authentication instanceof AnonymousAuthenticationToken)) {
|
||||
return "redirect:/mfuc/index.do";
|
||||
}
|
||||
|
||||
if (loginError != null) {
|
||||
model.addAttribute("loginMessage", "아이디 또는 비밀번호를 확인해주세요.");
|
||||
}
|
||||
|
||||
String referer = request.getHeader("referer");
|
||||
model.addAttribute("returnUrl", referer == null ? "/" : referer);
|
||||
return "kr/iotdoor/comn/user/login/lgnPge";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import kr.iotdoor.boot.model.contact.ContactRequest;
|
||||
import kr.iotdoor.boot.model.contact.ContactSubmission;
|
||||
import kr.iotdoor.boot.service.ContactSubmissionService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/contact")
|
||||
public class ContactApiController {
|
||||
|
||||
private final ContactSubmissionService contactSubmissionService;
|
||||
|
||||
public ContactApiController(ContactSubmissionService contactSubmissionService) {
|
||||
this.contactSubmissionService = contactSubmissionService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> submit(@Valid @RequestBody ContactRequest request) {
|
||||
ContactSubmission submission = contactSubmissionService.submit(request);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("message", "문의가 정상적으로 접수되었습니다.");
|
||||
body.put("submittedAt", submission.getSubmittedAt().toString());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidation(MethodArgumentNotValidException exception) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("success", false);
|
||||
|
||||
Map<String, String> errors = new LinkedHashMap<>();
|
||||
for (FieldError error : exception.getBindingResult().getFieldErrors()) {
|
||||
errors.putIfAbsent(error.getField(), error.getDefaultMessage());
|
||||
}
|
||||
|
||||
body.put("message", errors.values().stream().findFirst().orElse("입력값을 확인해주세요."));
|
||||
body.put("errors", errors);
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.model.file.StoredFile;
|
||||
import kr.iotdoor.boot.service.FileStorageService;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||
|
||||
@Controller
|
||||
public class FileController {
|
||||
|
||||
private final FileStorageService fileStorageService;
|
||||
|
||||
public FileController(FileStorageService fileStorageService) {
|
||||
this.fileStorageService = fileStorageService;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@PostMapping("/mfuc/file/upload.do")
|
||||
public ResponseEntity<Map<String, Object>> uploadFile(
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(name = "directory", defaultValue = "common") String directory,
|
||||
@RequestParam(name = "description", required = false) String description
|
||||
) {
|
||||
StoredFile storedFile = fileStorageService.storeSingleFile(file, directory, description);
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("success", true);
|
||||
body.put("atchFileIdntfNo", storedFile.getAtchFileIdntfNo());
|
||||
body.put("fileSn", storedFile.getFileSn());
|
||||
body.put("orgnlFileNm", storedFile.getOrgnlFileNm());
|
||||
body.put("url", storedFile.getUrl());
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@GetMapping("/comn/file/view/{atchFileIdntfNo}/{fileSn}")
|
||||
public ResponseEntity<UrlResource> viewFile(
|
||||
@PathVariable String atchFileIdntfNo,
|
||||
@PathVariable int fileSn
|
||||
) throws IOException {
|
||||
StoredFile storedFile = fileStorageService.getStoredFile(atchFileIdntfNo, fileSn);
|
||||
if (storedFile == null) {
|
||||
throw new ResponseStatusException(NOT_FOUND);
|
||||
}
|
||||
|
||||
Path filePath = fileStorageService.resolvePath(storedFile);
|
||||
if (!Files.exists(filePath)) {
|
||||
throw new ResponseStatusException(NOT_FOUND);
|
||||
}
|
||||
|
||||
UrlResource resource = new UrlResource(filePath.toUri());
|
||||
String contentType = Files.probeContentType(filePath);
|
||||
MediaType mediaType = StringUtils.hasText(contentType)
|
||||
? MediaType.parseMediaType(contentType)
|
||||
: MediaType.APPLICATION_OCTET_STREAM;
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(mediaType)
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
ContentDisposition.inline()
|
||||
.filename(storedFile.getOrgnlFileNm(), StandardCharsets.UTF_8)
|
||||
.build()
|
||||
.toString()
|
||||
)
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.config.IotdoorProperties;
|
||||
import kr.iotdoor.boot.model.user.AdminUserPrincipal;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ControllerAdvice(annotations = Controller.class)
|
||||
public class GlobalViewModelAdvice {
|
||||
|
||||
private final IotdoorProperties properties;
|
||||
|
||||
public GlobalViewModelAdvice(IotdoorProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@ModelAttribute("siteInfo")
|
||||
public IotdoorProperties.Site siteInfo() {
|
||||
return properties.getSite();
|
||||
}
|
||||
|
||||
@ModelAttribute("popupList")
|
||||
public List<Object> popupList() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@ModelAttribute("subMainClass")
|
||||
public String subMainClass() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@ModelAttribute("currentAdminName")
|
||||
public String currentAdminName(Authentication authentication) {
|
||||
if (authentication == null || !authentication.isAuthenticated() || authentication instanceof AnonymousAuthenticationToken) {
|
||||
return "";
|
||||
}
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal instanceof AdminUserPrincipal adminUserPrincipal) {
|
||||
return adminUserPrincipal.getDisplayName();
|
||||
}
|
||||
return properties.getSecurity().getAdminDisplayName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package kr.iotdoor.boot.web;
|
||||
|
||||
import kr.iotdoor.boot.service.PortfolioService;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class PublicPageController {
|
||||
|
||||
private final PortfolioService portfolioService;
|
||||
|
||||
public PublicPageController(PortfolioService portfolioService) {
|
||||
this.portfolioService = portfolioService;
|
||||
}
|
||||
|
||||
@GetMapping({"/", "/index.do"})
|
||||
public String home(Model model) {
|
||||
model.addAttribute("mainYn", "Y");
|
||||
return "kr/iotdoor/comn/site/mainPge";
|
||||
}
|
||||
|
||||
@GetMapping("/contact.do")
|
||||
public String contact() {
|
||||
return "kr/iotdoor/cmty/contact/contact";
|
||||
}
|
||||
|
||||
@GetMapping("/portfolio.do")
|
||||
public String portfolio(Model model) {
|
||||
model.addAttribute("portfolioItems", portfolioService.getPortfolioItems());
|
||||
return "kr/iotdoor/cmty/portfolio/portfolio";
|
||||
}
|
||||
|
||||
@GetMapping("/portfolio/{portfolioId}")
|
||||
public String portfolioDetail(@org.springframework.web.bind.annotation.PathVariable String portfolioId, Model model) {
|
||||
var portfolio = portfolioService.getPortfolioDetail(portfolioId);
|
||||
if (portfolio == null) {
|
||||
throw new org.springframework.web.server.ResponseStatusException(
|
||||
org.springframework.http.HttpStatus.NOT_FOUND,
|
||||
"포트폴리오를 찾을 수 없습니다."
|
||||
);
|
||||
}
|
||||
|
||||
model.addAttribute("portfolio", portfolio);
|
||||
return "kr/iotdoor/cmty/portfolio/detail";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
server:
|
||||
port: ${PORT:9090}
|
||||
servlet:
|
||||
session:
|
||||
timeout: 60m
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: iotdoor-boot
|
||||
devtools:
|
||||
restart:
|
||||
enabled: true
|
||||
poll-interval: 2s
|
||||
quiet-period: 500ms
|
||||
livereload:
|
||||
enabled: true
|
||||
thymeleaf:
|
||||
cache: false
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: ${IOTDOOR_MAX_FILE_SIZE:20MB}
|
||||
max-request-size: ${IOTDOOR_MAX_REQUEST_SIZE:100MB}
|
||||
datasource:
|
||||
# Legacy context.xml resource name: java:comp/env/jdbc/serviceDB
|
||||
url: ${IOTDOOR_DB_URL:jdbc:mariadb://localhost:3306/iotdoor2_db}
|
||||
username: ${IOTDOOR_DB_USERNAME:iotdoor_user}
|
||||
password: ${IOTDOOR_DB_PASSWORD:iotdoor_pass1!}
|
||||
driver-class-name: ${IOTDOOR_DB_DRIVER:org.mariadb.jdbc.Driver}
|
||||
hikari:
|
||||
pool-name: ${IOTDOOR_DB_POOL_NAME:IotdoorHikariPool}
|
||||
maximum-pool-size: ${IOTDOOR_DB_MAX_POOL_SIZE:20}
|
||||
minimum-idle: ${IOTDOOR_DB_MIN_IDLE:10}
|
||||
connection-test-query: SELECT 1
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
jackson:
|
||||
time-zone: Asia/Seoul
|
||||
mvc:
|
||||
problemdetails:
|
||||
enabled: true
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
|
||||
mybatis:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
|
||||
iotdoor:
|
||||
site:
|
||||
name: 국제오토텍
|
||||
admin-title: 국제오토텍 관리자
|
||||
email: iotdoor1749@naver.com
|
||||
phone: 042-585-1749
|
||||
phone-alt: 010-3476-1749
|
||||
address: 대전광역시 서구 유등로 55 102호
|
||||
summary: 자동문 전문 업체로 철거, 시공, A/S까지 원스톱으로 대응합니다.
|
||||
description: 슬라이딩자동문, 산업용자동문, 내풍압셔터, 오버헤드도어를 중심으로 시공과 유지보수를 제공합니다.
|
||||
map-latitude: 36.2980733
|
||||
map-longitude: 127.3814560
|
||||
map-client-id: ${IOTDOOR_NAVER_MAP_CLIENT_ID:xkb35c3i3k}
|
||||
security:
|
||||
admin-username: ${IOTDOOR_ADMIN_USERNAME:admin}
|
||||
admin-password: ${IOTDOOR_ADMIN_PASSWORD:ChangeMe123!}
|
||||
admin-display-name: ${IOTDOOR_ADMIN_DISPLAY_NAME:관리자}
|
||||
file:
|
||||
upload-path: ${IOTDOOR_FILE_UPLOAD_PATH:./uploads}
|
||||
@@ -0,0 +1,71 @@
|
||||
CREATE TABLE IF NOT EXISTS TN_USER (
|
||||
USER_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
USER_ID VARCHAR(100) NOT NULL,
|
||||
PSWD VARCHAR(200) NOT NULL,
|
||||
USER_NM VARCHAR(100) NOT NULL,
|
||||
EML_ADDR VARCHAR(200) NOT NULL,
|
||||
USER_STTS_CD VARCHAR(10) NOT NULL DEFAULT 'A',
|
||||
USER_TYPE_CD VARCHAR(10) NOT NULL DEFAULT 'ADM',
|
||||
JOIN_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INDVDL_INFO_AGRE_DT DATETIME NULL,
|
||||
LOCK_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||
LGN_FAIL_NMTM INT NULL DEFAULT 0,
|
||||
LOCK_DT DATETIME NULL,
|
||||
LAST_LGN_DT DATETIME NULL,
|
||||
PRIMARY KEY (USER_IDNTF_NO),
|
||||
UNIQUE (USER_ID)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_ATCH_FILE (
|
||||
ATCH_FILE_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
CRT_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
DEL_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||
PRIMARY KEY (ATCH_FILE_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_ATCH_FILE_DTL (
|
||||
ATCH_FILE_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
FILE_SN INT NOT NULL,
|
||||
FILE_STRG_PATH_NM VARCHAR(500) NOT NULL,
|
||||
FILE_STRG_NM VARCHAR(255) NOT NULL,
|
||||
ORGNL_FILE_NM VARCHAR(255) NOT NULL,
|
||||
FILE_EXTN_NM VARCHAR(50) NULL,
|
||||
FILE_CN VARCHAR(255) NULL,
|
||||
FILE_SZ BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (ATCH_FILE_IDNTF_NO, FILE_SN),
|
||||
CONSTRAINT FK_TN_ATCH_FILE_DTL__MASTER FOREIGN KEY (ATCH_FILE_IDNTF_NO)
|
||||
REFERENCES TN_ATCH_FILE (ATCH_FILE_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_PORTFOLIO (
|
||||
PORTFOLIO_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
TITLE VARCHAR(200) NOT NULL,
|
||||
THUMBNAIL_ATCH_FILE_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
THUMBNAIL_FILE_SN INT NOT NULL DEFAULT 1,
|
||||
CONTENT TEXT NOT NULL,
|
||||
REG_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
INQ_CNT BIGINT NOT NULL DEFAULT 0,
|
||||
REG_USER_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
DEL_YN CHAR(1) NOT NULL DEFAULT 'N',
|
||||
UPD_DT DATETIME NULL,
|
||||
PRIMARY KEY (PORTFOLIO_IDNTF_NO),
|
||||
CONSTRAINT FK_TN_PORTFOLIO__USER FOREIGN KEY (REG_USER_IDNTF_NO)
|
||||
REFERENCES TN_USER (USER_IDNTF_NO),
|
||||
CONSTRAINT FK_TN_PORTFOLIO__THUMBNAIL FOREIGN KEY (THUMBNAIL_ATCH_FILE_IDNTF_NO)
|
||||
REFERENCES TN_ATCH_FILE (ATCH_FILE_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TN_CONTACT_INQRY (
|
||||
CONTACT_IDNTF_NO VARCHAR(32) NOT NULL,
|
||||
NM VARCHAR(100) NOT NULL,
|
||||
EML_ADDR VARCHAR(200) NOT NULL,
|
||||
TELNO VARCHAR(30) NOT NULL,
|
||||
TTL VARCHAR(200) NOT NULL,
|
||||
CN TEXT NOT NULL,
|
||||
SUBMIT_DT DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (CONTACT_IDNTF_NO)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS IDX_TN_ATCH_FILE_DTL__PATH ON TN_ATCH_FILE_DTL (FILE_STRG_PATH_NM);
|
||||
CREATE INDEX IF NOT EXISTS IDX_TN_PORTFOLIO__REG_DT ON TN_PORTFOLIO (REG_DT);
|
||||
CREATE INDEX IF NOT EXISTS IDX_TN_CONTACT_INQRY__SUBMIT_DT ON TN_CONTACT_INQRY (SUBMIT_DT);
|
||||
@@ -0,0 +1,236 @@
|
||||
(function () {
|
||||
const EMAIL_JS_PUBLIC_KEY = 'd7I4hw_UMkVTo2UXy';
|
||||
const EMAIL_JS_SERVICE_ID = 'service_jfxkmcd';
|
||||
const EMAIL_JS_TEMPLATE_ID = 'template_vu49z25';
|
||||
|
||||
let emailJsInitialized = false;
|
||||
|
||||
function getCsrfHeader() {
|
||||
const token = document.querySelector('meta[name="_csrf"]');
|
||||
const header = document.querySelector('meta[name="_csrf_header"]');
|
||||
|
||||
return {
|
||||
token: token ? token.getAttribute('content') : '',
|
||||
header: header ? header.getAttribute('content') : ''
|
||||
};
|
||||
}
|
||||
|
||||
function isValidEmail(email) {
|
||||
return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/.test((email || '').trim());
|
||||
}
|
||||
|
||||
function isValidMobile(phone) {
|
||||
return /^01[016789]-?\d{3,4}-?\d{4}$/.test((phone || '').trim());
|
||||
}
|
||||
|
||||
function onlyNumber(value) {
|
||||
return value.replace(/[^0-9]/g, '');
|
||||
}
|
||||
|
||||
function formatMobile(phone) {
|
||||
const numbers = onlyNumber(phone);
|
||||
|
||||
if (numbers.length <= 3) {
|
||||
return numbers;
|
||||
}
|
||||
if (numbers.length <= 7) {
|
||||
return numbers.replace(/(\d{3})(\d{1,4})/, '$1-$2');
|
||||
}
|
||||
if (numbers.length <= 11) {
|
||||
return numbers.replace(/(\d{3})(\d{3,4})(\d{1,4})/, '$1-$2-$3');
|
||||
}
|
||||
|
||||
return numbers.substring(0, 11).replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
|
||||
}
|
||||
|
||||
function setErrorVisible(id, visible) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
element.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function spinner(on) {
|
||||
const spinnerElement = $('#spinner');
|
||||
if (!spinnerElement.length) {
|
||||
return;
|
||||
}
|
||||
spinnerElement.toggleClass('show', on);
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
return {
|
||||
name: document.getElementById('name').value.trim(),
|
||||
email: document.getElementById('email').value.trim(),
|
||||
phone: document.getElementById('phone').value.trim(),
|
||||
subject: document.getElementById('subject').value.trim(),
|
||||
message: document.getElementById('message').value.trim()
|
||||
};
|
||||
}
|
||||
|
||||
function validatePayload(payload) {
|
||||
if (!payload.name || !payload.email || !payload.phone || !payload.subject || !payload.message) {
|
||||
alert('필수 항목은 모두 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
if (!isValidEmail(payload.email)) {
|
||||
alert('올바른 이메일 형식이 아닙니다.');
|
||||
document.getElementById('email').focus();
|
||||
return false;
|
||||
}
|
||||
if (!isValidMobile(payload.phone)) {
|
||||
alert('올바른 휴대폰번호 형식이 아닙니다.');
|
||||
document.getElementById('phone').focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
['name', 'email', 'phone', 'subject', 'message'].forEach(function (id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.value = '';
|
||||
}
|
||||
});
|
||||
setErrorVisible('emailError', false);
|
||||
setErrorVisible('phoneError', false);
|
||||
}
|
||||
|
||||
function initEmailJs() {
|
||||
if (emailJsInitialized) {
|
||||
return true;
|
||||
}
|
||||
if (!window.emailjs || typeof window.emailjs.init !== 'function') {
|
||||
console.warn('EmailJS SDK could not be loaded.');
|
||||
return false;
|
||||
}
|
||||
|
||||
window.emailjs.init({
|
||||
publicKey: EMAIL_JS_PUBLIC_KEY
|
||||
});
|
||||
emailJsInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function sendContactToDatabase(payload) {
|
||||
const csrf = getCsrfHeader();
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
$.ajax({
|
||||
url: '/api/contact',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(payload),
|
||||
beforeSend: function (xhr) {
|
||||
if (csrf.header && csrf.token) {
|
||||
xhr.setRequestHeader(csrf.header, csrf.token);
|
||||
}
|
||||
}
|
||||
}).done(function (response) {
|
||||
resolve(response || {});
|
||||
}).fail(function (xhr) {
|
||||
reject(xhr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEmailJsErrorMessage(error) {
|
||||
if (!error) {
|
||||
return 'Unknown EmailJS error';
|
||||
}
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
if (error.text) {
|
||||
return error.text;
|
||||
}
|
||||
if (error.message) {
|
||||
return error.message;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(error);
|
||||
} catch (ignored) {
|
||||
return 'Unknown EmailJS error';
|
||||
}
|
||||
}
|
||||
|
||||
function sendContactMail(form) {
|
||||
if (!initEmailJs() || !window.emailjs || typeof window.emailjs.sendForm !== 'function') {
|
||||
return Promise.reject(new Error('EmailJS SDK is not available.'));
|
||||
}
|
||||
|
||||
return window.emailjs.sendForm(
|
||||
EMAIL_JS_SERVICE_ID,
|
||||
EMAIL_JS_TEMPLATE_ID,
|
||||
form,
|
||||
{
|
||||
publicKey: EMAIL_JS_PUBLIC_KEY
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function sendContact(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.currentTarget;
|
||||
const payload = buildPayload();
|
||||
if (!validatePayload(payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('btnSendMail');
|
||||
|
||||
spinner(true);
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await sendContactToDatabase(payload);
|
||||
|
||||
try {
|
||||
await sendContactMail(form);
|
||||
alert(response.message || '문의가 접수되었고 메일이 발송되었습니다.');
|
||||
} catch (error) {
|
||||
const errorMessage = getEmailJsErrorMessage(error);
|
||||
console.error('EmailJS send failed:', error);
|
||||
alert((response.message || '문의가 접수되었습니다.') + '\n메일 발송은 실패했습니다.\n사유: ' + errorMessage);
|
||||
}
|
||||
|
||||
resetForm();
|
||||
} catch (xhr) {
|
||||
const response = xhr.responseJSON || {};
|
||||
alert(response.message || '문의 접수 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
spinner(false);
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
const phoneInput = document.getElementById('phone');
|
||||
const emailInput = document.getElementById('email');
|
||||
const form = document.getElementById('contactForm');
|
||||
|
||||
initEmailJs();
|
||||
|
||||
if (phoneInput) {
|
||||
phoneInput.addEventListener('input', function (e) {
|
||||
e.target.value = formatMobile(e.target.value);
|
||||
});
|
||||
phoneInput.addEventListener('focusout', function () {
|
||||
setErrorVisible('phoneError', !!phoneInput.value && !isValidMobile(phoneInput.value));
|
||||
});
|
||||
}
|
||||
|
||||
if (emailInput) {
|
||||
emailInput.addEventListener('focusout', function () {
|
||||
setErrorVisible('emailError', !!emailInput.value && !isValidEmail(emailInput.value));
|
||||
});
|
||||
}
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', sendContact);
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
.login-container{
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.login-container .login-box {
|
||||
align-content: center;
|
||||
background-color: #fff;
|
||||
padding: 40px;
|
||||
margin: auto 0;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 5px 6px rgba(0, 0, 0, .1019607843);
|
||||
}
|
||||
@@ -0,0 +1,659 @@
|
||||
/********** Template CSS **********/
|
||||
:root {
|
||||
--primary: #254447;
|
||||
--light: #FFFFFF;
|
||||
--dark: #971f19;
|
||||
--gray: #9B9B9B;
|
||||
}
|
||||
|
||||
.fw-medium {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.fw-bold {
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.fw-black {
|
||||
font-weight: 900 !important;
|
||||
}
|
||||
|
||||
.back-to-top {
|
||||
position: fixed;
|
||||
display: none;
|
||||
right: 45px;
|
||||
bottom: 45px;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
|
||||
/*** Spinner ***/
|
||||
#spinner {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .5s ease-out, visibility 0s linear .5s;
|
||||
z-index: 99999;
|
||||
}
|
||||
|
||||
#spinner.show {
|
||||
transition: opacity .5s ease-out, visibility 0s linear 0s;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/*** Button ***/
|
||||
.btn {
|
||||
font-weight: 500;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.btn.btn-primary,
|
||||
.btn.btn-outline-primary:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.btn-square {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.btn-sm-square {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-lg-square {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.btn-square,
|
||||
.btn-sm-square,
|
||||
.btn-lg-square {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/*** Navbar ***/
|
||||
.navbar.sticky-top {
|
||||
top: -100px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.navbar .navbar-brand,
|
||||
.navbar a.btn {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav .nav-link {
|
||||
margin-right: 30px;
|
||||
padding: 25px 0;
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav .nav-link:hover,
|
||||
.navbar .navbar-nav .nav-link.active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.navbar .dropdown-toggle::after {
|
||||
border: none;
|
||||
content: "\f107";
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
vertical-align: middle;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.navbar .navbar-nav .nav-link {
|
||||
margin-right: 0;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav {
|
||||
border-top: 1px solid #EEEEEE;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.navbar .nav-item .dropdown-menu {
|
||||
display: block;
|
||||
border: none;
|
||||
margin-top: 0;
|
||||
top: 150%;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.navbar .nav-item:hover .dropdown-menu {
|
||||
top: 100%;
|
||||
visibility: visible;
|
||||
transition: .5s;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Header ***/
|
||||
.owl-carousel-inner {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-carousel .owl-carousel-item {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.header-carousel .owl-carousel-item img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.header-carousel .owl-carousel-item p {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots {
|
||||
position: absolute;
|
||||
width: 60px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 30px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots .owl-dot {
|
||||
position: relative;
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
margin: 5px 0;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0 0 30px rgba(255, 255, 255, .9);
|
||||
border-radius: 45px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots .owl-dot.active {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.header-carousel .owl-dots .owl-dot img {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
padding: 2px;
|
||||
border-radius: 45px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
background: linear-gradient(rgba(0, 0, 0, .1), rgba(0, 0, 0, .1)), url(../img/carousel-1.jpg) center center no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.breadcrumb-item + .breadcrumb-item::before {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
|
||||
/*** About ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.about {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.about-text {
|
||||
padding-right: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.about-text {
|
||||
padding-right: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.about-text {
|
||||
padding-right: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Service ***/
|
||||
.service-item {
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
position: relative;
|
||||
margin: -50px 0 25px 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--primary);
|
||||
background: #FFFFFF;
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.service-item:hover .service-icon {
|
||||
color: #FFFFFF;
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
|
||||
/*** Feature ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.feature {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
padding-left: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.feature-text {
|
||||
padding-left: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.feature-text {
|
||||
padding-left: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Project Portfolio ***/
|
||||
#portfolio-flters li {
|
||||
display: inline-block;
|
||||
font-weight: 500;
|
||||
color: var(--dark);
|
||||
cursor: pointer;
|
||||
transition: .5s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
#portfolio-flters li:hover,
|
||||
#portfolio-flters li.active {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.portfolio-img {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio-img::before,
|
||||
.portfolio-img::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
background: var(--dark);
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.portfolio-img::before {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.portfolio-img::after {
|
||||
right: 50%;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-img::before {
|
||||
width: 51%;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-img::after {
|
||||
width: 51%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.portfolio-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-btn {
|
||||
opacity: 1;
|
||||
transition-delay: .3s;
|
||||
}
|
||||
|
||||
|
||||
/*** Quote ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.quote {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
padding-right: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.quote-text {
|
||||
padding-right: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.quote-text {
|
||||
padding-right: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Team ***/
|
||||
.team-item {
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
}
|
||||
|
||||
.team-item img {
|
||||
border-radius: 8px 60px 0 0;
|
||||
}
|
||||
|
||||
.team-item .team-social {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
background: #FFFFFF;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
|
||||
/*** Testimonial ***/
|
||||
.testimonial-carousel::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.testimonial-carousel::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
background: linear-gradient(to left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.testimonial-carousel::before,
|
||||
.testimonial-carousel::after {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.testimonial-carousel::before,
|
||||
.testimonial-carousel::after {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-nav {
|
||||
position: absolute;
|
||||
width: 350px;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
opacity: 0;
|
||||
transition: .5s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.testimonial-carousel:hover .owl-nav {
|
||||
width: 300px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-nav .owl-prev,
|
||||
.testimonial-carousel .owl-nav .owl-next {
|
||||
position: relative;
|
||||
color: var(--primary);
|
||||
font-size: 45px;
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-nav .owl-prev:hover,
|
||||
.testimonial-carousel .owl-nav .owl-next:hover {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.testimonial-carousel .testimonial-img img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.testimonial-carousel .testimonial-img .btn-square {
|
||||
position: absolute;
|
||||
bottom: -19px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-item .testimonial-text {
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 0 45px rgba(0, 0, 0, .08);
|
||||
transform: scale(.8);
|
||||
transition: .5s;
|
||||
}
|
||||
|
||||
.testimonial-carousel .owl-item.center .testimonial-text {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
|
||||
/*** Contact ***/
|
||||
@media (min-width: 992px) {
|
||||
.container.contact {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
padding-right: calc(((100% - 960px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.contact-text {
|
||||
padding-right: calc(((100% - 1140px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1400px) {
|
||||
.contact-text {
|
||||
padding-right: calc(((100% - 1320px) / 2) + .75rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*** Footer ***/
|
||||
.footer .btn.btn-social {
|
||||
margin-right: 5px;
|
||||
color: #9B9B9B;
|
||||
border: 1px solid #9B9B9B;
|
||||
border-radius: 38px;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.footer .btn.btn-social:hover {
|
||||
color: var(--primary);
|
||||
border-color: var(--light);
|
||||
}
|
||||
|
||||
.footer .btn.btn-link {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: #9B9B9B;
|
||||
font-weight: normal;
|
||||
text-transform: capitalize;
|
||||
transition: .3s;
|
||||
}
|
||||
|
||||
.footer .btn.btn-link::before {
|
||||
position: relative;
|
||||
content: "\f105";
|
||||
font-family: "Font Awesome 5 Free";
|
||||
font-weight: 900;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.footer .btn.btn-link:hover {
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 1px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.footer .copyright {
|
||||
padding: 25px 0;
|
||||
border-top: 1px solid rgba(256, 256, 256, .1);
|
||||
}
|
||||
|
||||
.footer .copyright a {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.footer .copyright a:hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.bg-dark {
|
||||
background-color: #254447 !important;
|
||||
}
|
||||
.text-primary {
|
||||
color: #254447 !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #254447!important;
|
||||
border-color: #254447!important;
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #254447!important;
|
||||
border-color: #254447!important;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
color: #ffffff!important;
|
||||
}
|
||||
/*
|
||||
a {
|
||||
color: #254447!important;
|
||||
}
|
||||
*/
|
||||
.bg-primary {
|
||||
background-color: #254447!important;
|
||||
}
|
||||
.btn.btn-primary{
|
||||
color: #ffffff!important;
|
||||
}
|
||||
.footer .btn.btn-social{
|
||||
color: var(--light)!important;
|
||||
}
|
||||
|
||||
.navbar .navbar-nav .nav-link:hover, .navbar .navbar-nav .nav-link.active {
|
||||
color: var(--dark)!important;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
아래 부터 커스텀..
|
||||
*/
|
||||
|
||||
|
||||
.text-body {
|
||||
--bs-text-opacity: 1;
|
||||
color: var(--gray)!important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--gray);
|
||||
}
|
||||
|
||||
|
||||
.site-sub-top {
|
||||
height: 483px;
|
||||
background: url(../image/template/backtop2.png) top center no-repeat;
|
||||
background-color: #194244;
|
||||
}
|
||||
|
||||
|
||||
/* 이미지 기본 설정 */
|
||||
.card-image {
|
||||
transition: transform 0.4s ease;
|
||||
}
|
||||
.album-list .card:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.album-list .card:hover img.card-image {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
||||
.article-list .article-paging {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ag-paging-panel {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.4 MiB |
@@ -0,0 +1,21 @@
|
||||
<svg id="badge_ardor" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="51.836" height="42.288" viewBox="0 0 51.836 42.288">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_427" data-name="사각형 427" width="51.836" height="42.288" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1630" data-name="그룹 1630" clip-path="url(#clip-path)">
|
||||
<path id="패스_1092" data-name="패스 1092" d="M3.621,49.015a1.9,1.9,0,1,1-1.9,1.9,1.9,1.9,0,0,1,1.9-1.9" transform="translate(-0.459 -13.049)" fill="#d7deed"/>
|
||||
<path id="패스_1093" data-name="패스 1093" d="M64.557,7.664a2.529,2.529,0,1,1-2.529,2.529,2.528,2.528,0,0,1,2.529-2.529" transform="translate(-16.512 -2.041)" fill="#d7deed"/>
|
||||
<path id="패스_1094" data-name="패스 1094" d="M44.1,21.6a7.275,7.275,0,0,1,2.338-5.338A7.286,7.286,0,0,1,41.1,7.039,7.206,7.206,0,0,1,35.331,6.4a7.267,7.267,0,0,1-3.455-4.676A7.242,7.242,0,0,1,26.559,4.04a7.253,7.253,0,0,1-5.337-2.317A7.262,7.262,0,0,1,17.767,6.4,7.206,7.206,0,0,1,12,7.039a7.207,7.207,0,0,1-.641,5.771,7.26,7.26,0,0,1-4.676,3.455,7.283,7.283,0,0,1,0,10.654,7.268,7.268,0,0,1,4.676,3.454A7.21,7.21,0,0,1,12,36.146a7.287,7.287,0,0,1,9.227,5.337,7.237,7.237,0,0,1,10.653,0A7.287,7.287,0,0,1,41.1,36.146a7.286,7.286,0,0,1,5.337-9.227A7.263,7.263,0,0,1,44.1,21.6" transform="translate(-1.778 -0.459)" fill="#ffb95a"/>
|
||||
<path id="패스_1095" data-name="패스 1095" d="M30.191,15.367a9.857,9.857,0,1,1-9.868,9.868,9.862,9.862,0,0,1,9.868-9.868" transform="translate(-5.41 -4.091)" fill="#fcd884"/>
|
||||
<rect id="사각형_423" data-name="사각형 423" width="2.528" height="2.528" transform="translate(46.779 21.536) rotate(-45)"/>
|
||||
<rect id="사각형_424" data-name="사각형 424" width="2.528" height="2.528" transform="translate(44.474 39.236) rotate(-45)"/>
|
||||
<rect id="사각형_425" data-name="사각형 425" width="2.528" height="2.528" transform="translate(1.265 6.366) rotate(-45)"/>
|
||||
<rect id="사각형_426" data-name="사각형 426" width="2.528" height="2.528" transform="translate(0 30.387) rotate(-45)"/>
|
||||
<path id="패스_1096" data-name="패스 1096" d="M31.417,42.287a1.262,1.262,0,0,1-.927-.405,5.973,5.973,0,0,0-8.8,0,1.264,1.264,0,0,1-2.16-.577,6.017,6.017,0,0,0-5.859-4.668,6.16,6.16,0,0,0-1.772.26,1.264,1.264,0,0,1-1.571-1.586,5.991,5.991,0,0,0-4.391-7.619,1.265,1.265,0,0,1-.578-2.16,6,6,0,0,0,1.912-4.39A6.031,6.031,0,0,0,5.36,16.732a1.264,1.264,0,0,1,.579-2.159,5.991,5.991,0,0,0,4.39-7.617,1.264,1.264,0,0,1,1.582-1.582,6.038,6.038,0,0,0,1.79.272,5.932,5.932,0,0,0,2.972-.8A5.963,5.963,0,0,0,19.531.982,1.264,1.264,0,0,1,21.69.4,6.023,6.023,0,0,0,26.1,2.316,6,6,0,0,0,30.489.4a1.264,1.264,0,0,1,2.159.578,5.967,5.967,0,0,0,2.857,3.863,5.937,5.937,0,0,0,2.972.8,6.038,6.038,0,0,0,1.79-.272,1.264,1.264,0,0,1,1.586,1.571,6.019,6.019,0,0,0,4.406,7.628,1.265,1.265,0,0,1,.579,2.16,5.974,5.974,0,0,0,0,8.8,1.264,1.264,0,0,1-.577,2.16,6.02,6.02,0,0,0-4.428,7.558,1.285,1.285,0,0,1,.077.437,1.265,1.265,0,0,1-1.251,1.264,1.232,1.232,0,0,1-.379-.054,6.166,6.166,0,0,0-1.773-.261A6.018,6.018,0,0,0,32.649,41.3a1.262,1.262,0,0,1-1.232.983m-17.744-8.18a8.538,8.538,0,0,1,7.661,4.775,8.471,8.471,0,0,1,9.511,0,8.542,8.542,0,0,1,7.661-4.773q.291,0,.58.019a8.531,8.531,0,0,1,4.752-8.238,8.47,8.47,0,0,1,0-9.512,8.532,8.532,0,0,1-4.754-8.224q-.3.021-.608.021a8.457,8.457,0,0,1-4.24-1.141A8.6,8.6,0,0,1,30.844,3.4,8.544,8.544,0,0,1,26.1,4.845,8.592,8.592,0,0,1,21.336,3.4a8.6,8.6,0,0,1-3.394,3.632A8.466,8.466,0,0,1,13.7,8.174c-.2,0-.4-.007-.594-.021a8.5,8.5,0,0,1-1.118,4.832A8.6,8.6,0,0,1,8.357,16.38,8.594,8.594,0,0,1,9.8,21.143a8.55,8.55,0,0,1-1.443,4.745,8.6,8.6,0,0,1,3.632,3.392,8.508,8.508,0,0,1,1.118,4.847c.188-.012.377-.018.566-.018" transform="translate(-1.319 0)"/>
|
||||
<path id="패스_1097" data-name="패스 1097" d="M29.732,35.887a11.121,11.121,0,1,1,11.11-11.11,11.134,11.134,0,0,1-11.11,11.11m0-19.714a8.593,8.593,0,1,0,8.582,8.6,8.614,8.614,0,0,0-8.582-8.6" transform="translate(-4.952 -3.632)"/>
|
||||
<path id="패스_1098" data-name="패스 1098" d="M3.162,53.614a3.161,3.161,0,1,1,3.16-3.161,3.165,3.165,0,0,1-3.16,3.161m0-3.793a.632.632,0,1,0,.633.632.632.632,0,0,0-.633-.632" transform="translate(-0.001 -12.59)"/>
|
||||
<path id="패스_1099" data-name="패스 1099" d="M64.1,13.527a3.793,3.793,0,1,1,3.793-3.793A3.8,3.8,0,0,1,64.1,13.527m0-5.057a1.264,1.264,0,1,0,1.264,1.264A1.266,1.266,0,0,0,64.1,8.47" transform="translate(-16.054 -1.582)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg id="badge_ardor" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="55.287" height="34.029" viewBox="0 0 55.287 34.029">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_421" data-name="사각형 421" width="55.287" height="34.029" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1626" data-name="그룹 1626" clip-path="url(#clip-path)">
|
||||
<path id="패스_1073" data-name="패스 1073" d="M35.448,26.166V48c0,1.979,7.523,3.583,16.8,3.583s16.8-1.6,16.8-3.583V26.166Z" transform="translate(-24.607 -18.164)" fill="#36464f"/>
|
||||
<path id="패스_1074" data-name="패스 1074" d="M50.862,50.8c-6.023,0-17.413-.877-17.413-4.195V24.778a.611.611,0,0,1,.612-.612h33.6a.611.611,0,0,1,.612.612V46.609c0,3.318-11.39,4.195-17.414,4.195M34.672,25.389v21.22c0,1.009,5.715,2.971,16.19,2.971s16.191-1.963,16.191-2.971V25.389Z" transform="translate(-23.219 -16.775)" fill="#231815"/>
|
||||
<path id="패스_1075" data-name="패스 1075" d="M29.166,2.025,55.824,12.3A.374.374,0,0,1,55.8,13L29.141,21.177a.372.372,0,0,1-.219,0L2.264,13a.374.374,0,0,1-.025-.706L28.9,2.025a.374.374,0,0,1,.269,0" transform="translate(-1.388 -1.389)" fill="#4b616e"/>
|
||||
<path id="패스_1076" data-name="패스 1076" d="M111.843,43.934a.611.611,0,0,1-.612-.612V32.638L91.689,28.19A.612.612,0,0,1,91.96,27l20.018,4.556a.612.612,0,0,1,.476.6V43.322a.612.612,0,0,1-.612.612" transform="translate(-63.318 -18.73)" fill="#231815"/>
|
||||
<path id="패스_1077" data-name="패스 1077" d="M27.644,20.417a.986.986,0,0,1-.289-.044L.7,12.2a.986.986,0,0,1-.066-1.862L27.29.066a.982.982,0,0,1,.708,0L54.656,10.338a.985.985,0,0,1-.065,1.862L27.933,20.373a.981.981,0,0,1-.289.044M1.714,11.232l25.93,7.95,25.93-7.95L27.644,1.24Zm52.5.248h0ZM27.558,1.207h0" transform="translate(-0.001 0)" fill="#231815"/>
|
||||
<path id="패스_1078" data-name="패스 1078" d="M86.45,27.586c0,.784-1.389,1.42-3.1,1.42s-3.1-.636-3.1-1.42,1.389-1.42,3.1-1.42,3.1.636,3.1,1.42" transform="translate(-55.704 -18.164)" fill="#f7546a"/>
|
||||
<path id="패스_1079" data-name="패스 1079" d="M81.959,28.228c-2.152,0-3.714-.854-3.714-2.031s1.562-2.031,3.714-2.031,3.714.854,3.714,2.031-1.562,2.031-3.714,2.031m0-2.839c-1.634,0-2.491.6-2.491.808s.856.808,2.491.808,2.491-.6,2.491-.808-.857-.808-2.491-.808" transform="translate(-54.316 -16.775)" fill="#231815"/>
|
||||
<path id="패스_1080" data-name="패스 1080" d="M34.716,27.5l6.02-2.176c1.837-.664,1.045-3.62-.813-2.949L33.9,24.546c-1.837.664-1.045,3.62.813,2.949" transform="translate(-22.811 -15.461)" fill="#fff"/>
|
||||
<path id="패스_1081" data-name="패스 1081" d="M67.376,18.771a1.53,1.53,0,0,0,0-3.058,1.53,1.53,0,0,0,0,3.058" transform="translate(-45.745 -10.908)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,18 @@
|
||||
<svg id="badge_atten" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="46.926" height="46.926" viewBox="0 0 46.926 46.926">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_430" data-name="사각형 430" width="46.926" height="46.926" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1638" data-name="그룹 1638" clip-path="url(#clip-path)">
|
||||
<path id="패스_1101" data-name="패스 1101" d="M47.519,24.759A22.759,22.759,0,1,1,24.759,2,22.759,22.759,0,0,1,47.519,24.759" transform="translate(-1.296 -1.296)" fill="#f0f0f0"/>
|
||||
<path id="패스_1102" data-name="패스 1102" d="M51.88,32.559A17.582,17.582,0,1,1,34.3,14.977,17.582,17.582,0,0,1,51.88,32.559" transform="translate(-10.835 -9.708)" fill="#6dc2f7"/>
|
||||
<path id="패스_1103" data-name="패스 1103" d="M51.46,28.738a17.581,17.581,0,1,0-27.8,17.814c11.415-2.21,21.9-8.217,27.8-17.814" transform="translate(-10.836 -9.708)" fill="#6dc2f7"/>
|
||||
<path id="패스_1104" data-name="패스 1104" d="M36.441,71.907a17.579,17.579,0,0,0,27.8-17.814c-5.9,9.6-16.389,15.6-27.8,17.814" transform="translate(-23.621 -35.063)" fill="#5ca2cf"/>
|
||||
<path id="패스_1105" data-name="패스 1105" d="M23.463,46.926A23.463,23.463,0,1,1,46.926,23.463,23.49,23.49,0,0,1,23.463,46.926m0-45.519A22.056,22.056,0,1,0,45.519,23.463,22.081,22.081,0,0,0,23.463,1.407" fill="#231815"/>
|
||||
<path id="패스_1106" data-name="패스 1106" d="M33,51.289A18.286,18.286,0,1,1,51.288,33,18.307,18.307,0,0,1,33,51.289m0-35.164A16.879,16.879,0,1,0,49.881,33,16.9,16.9,0,0,0,33,16.124" transform="translate(-9.539 -9.54)" fill="#231815"/>
|
||||
<path id="패스_1107" data-name="패스 1107" d="M49.4,53.416a.706.706,0,0,1-.282-.059l-8.685-3.8a.7.7,0,1,1,.564-1.289l8.347,3.652,10.809-6.69a.7.7,0,0,1,.741,1.2L49.766,53.311a.707.707,0,0,1-.37.105" transform="translate(-25.932 -29.25)" fill="#231815"/>
|
||||
<path id="패스_1108" data-name="패스 1108" d="M36.766,29.357c1.191-2.647,3.833-3.509,6.483-4.084,2.211-.481,1.275-3.873-.935-3.392-3.473.754-7.039,2.264-8.586,5.7-.922,2.048,2.109,3.839,3.038,1.776" transform="translate(-21.75 -14.153)" fill="#fff"/>
|
||||
<path id="패스_1109" data-name="패스 1109" d="M70.761,27.162a1.76,1.76,0,0,0,0-3.518,1.76,1.76,0,0,0,0,3.518" transform="translate(-44.766 -15.326)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,39 @@
|
||||
<svg id="badge_cooperation" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="59.738" height="34.392" viewBox="0 0 59.738 34.392">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_419" data-name="사각형 419" width="59.738" height="34.392" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1622" data-name="그룹 1622" clip-path="url(#clip-path)">
|
||||
<path id="패스_1028" data-name="패스 1028" d="M29.447,9.834a1.25,1.25,0,1,1-1.772,0,1.248,1.248,0,0,1,1.772,0" transform="translate(19.716 6.833)" fill="#bf542e"/>
|
||||
<path id="패스_1029" data-name="패스 1029" d="M8.242,9.349a1.269,1.269,0,0,1,.46,1.718,1.253,1.253,0,1,1-.46-1.718" transform="translate(4.595 6.633)" fill="#168f91"/>
|
||||
<path id="패스_1030" data-name="패스 1030" d="M29.109,3.1l5.035,8.723L37.5,17.633,30.72,21.541l-6.759-11.7-.126-.217-1.88-3.267-.15-.25L21.7,5.925l6.783-3.919.629,1.088ZM33.5,16.985a1.25,1.25,0,1,0-1.772,0,1.248,1.248,0,0,0,1.772,0" transform="translate(15.665 1.449)" fill="#bf542e"/>
|
||||
<path id="패스_1031" data-name="패스 1031" d="M33.023.285l7.451,12.9-7.022,4.055-2.416-4.183L26,4.336Z" transform="translate(18.773 0.206)" fill="#ff703d"/>
|
||||
<rect id="사각형_418" data-name="사각형 418" width="14.901" height="8.105" transform="translate(0.491 13.397) rotate(-60.013)" fill="#168f91"/>
|
||||
<path id="패스_1032" data-name="패스 1032" d="M13.086,1.658l6.783,3.909-.487.849-.028.024a.813.813,0,0,0-.071.064c-.022.019-.043.041-.062.06l-.007.01a.65.65,0,0,0-.065.074.75.75,0,0,0-.055.074l0,0a.85.85,0,0,0-.046.072.5.5,0,0,0-.033.059l-.021.038,0,.009a.609.609,0,0,0-.028.059.829.829,0,0,0-.048.129.947.947,0,0,0-.028.1l-.009.046a.578.578,0,0,0-.012.076l-1.744,3.022-4.53,7.838-1.462,2.538-.15.26-.129.222-.129-.077,0,0L4.068,17.285ZM10.36,16.5a1.252,1.252,0,1,0-2.166-1.255A1.252,1.252,0,1,0,10.36,16.5" transform="translate(2.937 1.197)" fill="#168f91"/>
|
||||
<path id="패스_1033" data-name="패스 1033" d="M19.8,20.1l-.539.935L16.8,22.463a1.959,1.959,0,0,1-2-3.368l-1.925,1.111a1.959,1.959,0,0,1-2-3.37L8.587,18.167a1.955,1.955,0,1,1-1.958-3.385l2.945-1.708.131.076.129-.222.222-.124,1.5,2.538Z" transform="translate(4.08 9.244)" fill="#fbd5a6"/>
|
||||
<path id="패스_1034" data-name="패스 1034" d="M18.955,19.607l-3,1.737a1.959,1.959,0,0,1-2-3.368l.539-.935,1.365.789,2.876,1.662a1.093,1.093,0,0,0,.133.071c.029.014.06.028.091.045" transform="translate(9.395 12.303)" fill="#fbd5a6"/>
|
||||
<path id="패스_1035" data-name="패스 1035" d="M35.018,16.52l1.471,2.548L20.635,9.551,20.6,9.532c-2.867.009-5.39-.281-7.143-1.922-.033-.031-.064-.064-.095-.095a1.968,1.968,0,0,1-.174-.189,3.308,3.308,0,0,1-.215-.289h0a2.055,2.055,0,0,1-.288-.684v0a1.371,1.371,0,0,1-.019-.449.571.571,0,0,1,.01-.076l.01-.046a.975.975,0,0,1,.026-.1.9.9,0,0,1,.048-.129c.009-.019.017-.04.028-.059l0-.009a.206.206,0,0,1,.021-.038c.01-.019.021-.038.033-.057s.031-.05.048-.074l0,0,.057-.074c.021-.024.041-.05.064-.074l.009-.009.06-.062c.024-.022.046-.043.071-.064l.028-.022c.833-.692,2.688-.992,5.781-.31,2.609.58,4.341-3.089,8.36.549l1.012-.3,2.032,3.516.124.219Z" transform="translate(9.136 2.609)" fill="#ddb992"/>
|
||||
<path id="패스_1036" data-name="패스 1036" d="M14.159,7.963l1.746-3.02a1.371,1.371,0,0,0,.019.449v0a1.965,1.965,0,0,0,.288.682l0,0a2.9,2.9,0,0,0,.215.288,1.968,1.968,0,0,0,.174.189c.029.031.062.064.095.095,1.753,1.641,4.276,1.93,7.143,1.922l15.888,9.536a2.246,2.246,0,0,1,.282,1.092,2.212,2.212,0,0,1-.294,1.109,2.254,2.254,0,0,1-3.046.82l-2.607-1.515a2.228,2.228,0,0,1-2.275,3.831l-2.194-1.264a2.228,2.228,0,0,1-2.276,3.831l-2.807-1.62a2.23,2.23,0,0,1-2.149,3.9c-.043-.021-.088-.045-.133-.071l-2.876-1.66-1.365-.79L9.741,21.014,8.167,18.34Z" transform="translate(5.896 3.569)" fill="#fbd5a6"/>
|
||||
<path id="패스_1037" data-name="패스 1037" d="M20.5,25.242a2.446,2.446,0,0,1-2.128-1.226,2.4,2.4,0,0,1-.307-1.507l-.813.47a2.459,2.459,0,0,1-3.344-.9,2.428,2.428,0,0,1-.3-1.515l-.279.16a2.448,2.448,0,0,1-3.654-2.407l-.635.368a2.445,2.445,0,0,1-3.583-2.769,2.412,2.412,0,0,1,1.138-1.469l2.939-1.7.014-.007a.492.492,0,0,1,.487.01l.129.077a.49.49,0,0,1-.363.9L7.081,15.3a1.439,1.439,0,0,0-.682.878,1.464,1.464,0,0,0,2.147,1.66l2.29-1.329a.49.49,0,0,1,.506.839,1.492,1.492,0,0,0-.508,1.994,1.47,1.47,0,0,0,2,.536l1.927-1.111a.49.49,0,0,1,.5.844,1.469,1.469,0,0,0,1.5,2.524L19.226,20.7a.49.49,0,0,1,.5.844,1.468,1.468,0,0,0,1.5,2.523L23.763,22.6a.458.458,0,0,1,.079-.229.489.489,0,0,1,.678-.141l.065.034.015.007.029.014c.022.012.046.022.071.036a.49.49,0,0,1,.019.859L21.714,24.92a2.449,2.449,0,0,1-1.216.322" transform="translate(3.874 9.151)"/>
|
||||
<path id="패스_1038" data-name="패스 1038" d="M10.009,16.036a.489.489,0,0,1-.422-.241L8.3,13.622a.48.48,0,0,1-.444-.25.489.489,0,0,1,.188-.666l.222-.126a.491.491,0,0,1,.663.177l1.5,2.538a.491.491,0,0,1-.422.74" transform="translate(5.628 9.037)"/>
|
||||
<path id="패스_1039" data-name="패스 1039" d="M23.55,29.221a2.705,2.705,0,0,1-1.2-.281c-.057-.028-.112-.057-.165-.09L9.7,21.644a.509.509,0,0,1-.177-.176L7.949,18.795a.489.489,0,0,1,0-.492l1.462-2.538L15.686,4.9a.49.49,0,0,1,.911.3.915.915,0,0,0,.014.3.491.491,0,0,1-.387.575.529.529,0,0,1-.091.009L8.939,18.543,10.3,20.859,22.68,28.005a1.112,1.112,0,0,0,.107.057,1.738,1.738,0,0,0,2.271-.689,1.757,1.757,0,0,0-.6-2.357.49.49,0,0,1,.5-.842l2.807,1.62a1.734,1.734,0,0,0,2.369-.63,1.759,1.759,0,0,0-.594-2.359.49.49,0,0,1,.5-.842l2.194,1.264a1.708,1.708,0,0,0,1.31.176,1.732,1.732,0,0,0,1.061-.808,1.757,1.757,0,0,0-.6-2.357.49.49,0,0,1,.5-.842l2.609,1.515a1.751,1.751,0,0,0,2.375-.642,1.729,1.729,0,0,0,.229-.863,1.751,1.751,0,0,0-.165-.744L23.8,9.205a.49.49,0,0,1,.479-.854l.034.019.012.007,15.856,9.517a.505.505,0,0,1,.176.181,2.719,2.719,0,0,1-3.728,3.687l-.858-.5a2.71,2.71,0,0,1-4.029,2.812l-.443-.255a2.709,2.709,0,0,1-4.028,2.822l-1.052-.608a2.707,2.707,0,0,1-2.674,3.187" transform="translate(5.69 3.364)"/>
|
||||
<path id="패스_1040" data-name="패스 1040" d="M13.367,6.67a.487.487,0,0,1-.339-.136l-.1-.1a.49.49,0,1,1,.694-.692c.029.028.059.057.088.086a.49.49,0,0,1-.339.844" transform="translate(9.227 4.04)"/>
|
||||
<path id="패스_1041" data-name="패스 1041" d="M13.261,6.587a.5.5,0,0,1-.375-.174,3.575,3.575,0,0,1-.248-.334.49.49,0,1,1,.816-.541c.059.088.119.167.181.243a.49.49,0,0,1-.374.806" transform="translate(9.064 3.839)"/>
|
||||
<path id="패스_1042" data-name="패스 1042" d="M13.166,6.583a.492.492,0,0,1-.413-.224,2.516,2.516,0,0,1-.356-.851.49.49,0,0,1,.961-.2,1.535,1.535,0,0,0,.22.515.492.492,0,0,1-.146.678.5.5,0,0,1-.265.077" transform="translate(8.942 3.552)"/>
|
||||
<path id="패스_1043" data-name="패스 1043" d="M11.053,21.889a.486.486,0,0,1-.245-.065L4.028,17.915a.489.489,0,0,1-.227-.3.484.484,0,0,1,.048-.372L12.866,1.619a.49.49,0,0,1,.67-.179l6.783,3.909a.49.49,0,0,1,.181.668l-.489.849a.489.489,0,1,1-.849-.487l.245-.425-5.936-3.42L4.942,17.311l5.932,3.42.033-.059,7.737-13.4a.49.49,0,0,1,.849.491L11.477,21.645a.481.481,0,0,1-.3.227.464.464,0,0,1-.127.017" transform="translate(2.731 0.992)"/>
|
||||
<path id="패스_1044" data-name="패스 1044" d="M12.872,5.655a.38.38,0,0,1-.088-.009.489.489,0,0,1-.394-.568l.009-.041,0-.014a1.478,1.478,0,0,1,.043-.16.49.49,0,1,1,.93.31.243.243,0,0,0-.01.038l-.009.041a.492.492,0,0,1-.482.4" transform="translate(8.939 3.27)"/>
|
||||
<path id="패스_1045" data-name="패스 1045" d="M12.92,5.452a.494.494,0,0,1-.184-.036.485.485,0,0,1-.269-.63.967.967,0,0,1,.043-.1.491.491,0,0,1,.876.441l-.01.022a.5.5,0,0,1-.456.3" transform="translate(8.975 3.191)"/>
|
||||
<path id="패스_1046" data-name="패스 1046" d="M12.939,5.411a.478.478,0,0,1-.2-.043.488.488,0,0,1-.25-.646.608.608,0,0,1,.06-.11.493.493,0,1,1,.387.8" transform="translate(8.988 3.172)"/>
|
||||
<path id="패스_1047" data-name="패스 1047" d="M12.969,5.37a.491.491,0,0,1-.429-.728c.024-.041.05-.081.079-.122a.49.49,0,1,1,.794.573l-.015.026a.491.491,0,0,1-.429.251" transform="translate(9.009 3.116)"/>
|
||||
<path id="패스_1048" data-name="패스 1048" d="M13,5.352a.473.473,0,0,1-.262-.077.484.484,0,0,1-.152-.668.988.988,0,0,1,.09-.122,1.2,1.2,0,0,1,.088-.1.49.49,0,1,1,.713.673.224.224,0,0,0-.033.038l-.01.012-.019.022A.5.5,0,0,1,13,5.352" transform="translate(9.031 3.053)"/>
|
||||
<path id="패스_1049" data-name="패스 1049" d="M13.073,5.255a.49.49,0,0,1-.358-.825c.026-.028.053-.057.083-.083s.062-.057.093-.084a.49.49,0,1,1,.629.752l-.038.034-.024.022-.026.026a.49.49,0,0,1-.358.157" transform="translate(9.085 2.997)"/>
|
||||
<path id="패스_1050" data-name="패스 1050" d="M7.824,12.385h0a1.757,1.757,0,0,1-.875-.234,1.738,1.738,0,0,1-.634-2.38,1.742,1.742,0,1,1,1.508,2.614m.005-2.5a.767.767,0,0,0-.744.959.747.747,0,0,0,.355.461.773.773,0,0,0,.384.1.758.758,0,0,0,.66-.379A.777.777,0,0,0,8.2,9.978a.762.762,0,0,0-.372-.1" transform="translate(4.389 6.427)"/>
|
||||
<path id="패스_1051" data-name="패스 1051" d="M33.659,17.94a.49.49,0,0,1-.425-.246L25.783,4.788a.489.489,0,0,1,.086-.6.484.484,0,0,1,.095-.071L32.984.067a.49.49,0,0,1,.67.179l7.453,12.9a.491.491,0,0,1-.181.67L33.9,17.873a.489.489,0,0,1-.245.067M26.876,4.723,33.838,16.78l6.173-3.564L33.049,1.16Z" transform="translate(18.566 0.001)"/>
|
||||
<path id="패스_1052" data-name="패스 1052" d="M36.694,19.762a.5.5,0,0,1-.253-.069L20.67,10.226h-.005c-2.69,0-5.426-.265-7.337-2.053-.041-.041-.079-.077-.114-.114a2.718,2.718,0,0,1-.2-.215,3.061,3.061,0,0,1-.251-.338l-.007-.01a2.481,2.481,0,0,1-.346-.82l-.01-.052a1.848,1.848,0,0,1-.021-.567A.776.776,0,0,1,12.4,5.94c0-.024.009-.045.014-.067a1.12,1.12,0,0,1,.036-.134,1.354,1.354,0,0,1,.069-.186c.009-.019.022-.05.036-.076a.558.558,0,0,1,.033-.064c.015-.029.033-.059.05-.088s.031-.048.052-.077l.024-.033c.017-.026.038-.052.057-.076l.01-.012c.022-.028.046-.055.071-.083l.031-.034.062-.06.01-.01c.026-.026.053-.05.081-.074l.036-.031c1.1-.907,3.3-1.054,6.2-.413a3.858,3.858,0,0,0,2.321-.456c1.5-.618,3.36-1.381,6.054.938l.752-.222a.5.5,0,0,1,.56.219l.15.25L31,8.423a.525.525,0,0,1,.048.117.447.447,0,0,1,.076.1l6,10.385a.489.489,0,0,1-.424.735M20.806,9.246a.478.478,0,0,1,.251.071l14.266,8.561L30.273,9.13a.532.532,0,0,1-.05-.117.578.578,0,0,1-.076-.1L28.315,5.728l-.649.191a.487.487,0,0,1-.467-.107c-2.349-2.127-3.749-1.551-5.231-.942a4.686,4.686,0,0,1-2.907.506c-3.642-.8-4.963-.121-5.36.208l-.01.009-.015.014c-.015.012-.029.026-.045.04l-.043.045h0l-.048.055L13.5,5.8l-.005.007-.024.038L13.44,5.9l0,0-.007.014-.015.031a.4.4,0,0,0-.026.069l-.007.022-.012.041-.007.034,0,.017,0,.04a.863.863,0,0,0,.012.288l0,.015a1.475,1.475,0,0,0,.217.5l.01.017a2.2,2.2,0,0,0,.167.222l.009.01a1.691,1.691,0,0,0,.139.152c.033.034.06.06.088.086,1.689,1.582,4.27,1.791,6.8,1.786Z" transform="translate(8.93 2.405)"/>
|
||||
<path id="패스_1053" data-name="패스 1053" d="M30.926,22.236a.488.488,0,0,1-.424-.245L21.481,6.374a.486.486,0,0,1,.179-.668l6.783-3.919a.49.49,0,0,1,.668.179l9.02,15.627a.493.493,0,0,1-.179.67L31.17,22.171a.478.478,0,0,1-.245.065M22.574,6.308,31.1,21.077l5.932-3.42L28.508,2.88Z" transform="translate(15.46 1.243)"/>
|
||||
<path id="패스_1054" data-name="패스 1054" d="M28.768,12.666A1.746,1.746,0,1,1,30,12.155a1.727,1.727,0,0,1-1.229.511m0-2.505a.757.757,0,0,0-.539.224.775.775,0,0,0-.224.542.745.745,0,0,0,.22.53.76.76,0,0,0,.542.227.767.767,0,0,0,.761-.758.759.759,0,0,0-.226-.542.743.743,0,0,0-.536-.224" transform="translate(19.51 6.629)"/>
|
||||
<path id="패스_1055" data-name="패스 1055" d="M7.51,17.939a.469.469,0,0,1-.245-.065L.245,13.82a.49.49,0,0,1-.179-.67L7.517.246a.483.483,0,0,1,.3-.229.492.492,0,0,1,.372.05L15.2,4.12a.49.49,0,0,1,.181.668L7.933,17.694a.489.489,0,0,1-.3.229.537.537,0,0,1-.127.015M1.159,13.215,7.331,16.78,14.287,4.723,8.119,1.16Z" transform="translate(0 0.001)"/>
|
||||
<path id="패스_1056" data-name="패스 1056" d="M27.84,4.465a.483.483,0,0,1-.329-.127c-2.345-2.128-3.749-1.55-5.233-.938a4.716,4.716,0,0,1-2.9.513c-3.568-.794-4.918-.143-5.333.172a.49.49,0,1,1-.594-.778c1.123-.858,3.3-.982,6.139-.351A3.86,3.86,0,0,0,21.9,2.494c1.541-.635,3.461-1.426,6.264,1.119a.489.489,0,0,1-.329.852" transform="translate(9.569 1.342)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg id="badge_cooperation" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="34.83" height="39.384" viewBox="0 0 34.83 39.384">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_420" data-name="사각형 420" width="34.83" height="39.384" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1624" data-name="그룹 1624" clip-path="url(#clip-path)">
|
||||
<path id="패스_1057" data-name="패스 1057" d="M34.83,23.014v8.555a7.819,7.819,0,0,1-15.637,0v-8.1a7.263,7.263,0,0,1-1.767.223,7.439,7.439,0,0,1-1.789-.223v8.1A7.819,7.819,0,0,1,0,31.569V12.448A7.819,7.819,0,0,1,3.627,5.861V4.274A4.235,4.235,0,0,1,7.772,0l.086.682L7.865,0A4.235,4.235,0,0,1,12.01,4.274V5.861a7.809,7.809,0,0,1,3.627,6.587v.919a5.952,5.952,0,0,0,1.781.273,5.87,5.87,0,0,0,1.774-.273v-.919a7.8,7.8,0,0,1,3.678-6.616V4.274A4.235,4.235,0,0,1,27.015,0l.093.682V0A4.236,4.236,0,0,1,31.26,4.274V5.89a7.813,7.813,0,0,1,3.57,6.558V23.014Zm-1.35,4.159V23.287a8.809,8.809,0,0,0-6.465-2.593,9.267,9.267,0,0,0-5.4,1.645,7.271,7.271,0,0,1-1.063.632v4.2a7.812,7.812,0,0,1,12.929,0m0-5.732v-6.86A8.81,8.81,0,0,0,27.015,12a9.267,9.267,0,0,0-5.4,1.645,7.191,7.191,0,0,1-8.39,0A9.267,9.267,0,0,0,7.822,12a8.8,8.8,0,0,0-6.465,2.579v6.853a10.633,10.633,0,0,1,6.465-2.1,10.63,10.63,0,0,1,6.192,1.9,5.786,5.786,0,0,0,3.4,1.092h.007a5.754,5.754,0,0,0,3.4-1.092,11.012,11.012,0,0,1,12.664.2m0-8.677v-.316a6.465,6.465,0,0,0-3.3-5.631,6.561,6.561,0,0,0-5.524-.374.1.1,0,0,0-.043.014,6.389,6.389,0,0,0-.711.33,6.48,6.48,0,0,0-3.347,5.66v.259c.086-.057.18-.1.266-.158a10.652,10.652,0,0,1,6.2-1.9,10.542,10.542,0,0,1,6.465,2.119m0,18.805a6.465,6.465,0,1,0-6.465,6.465,6.474,6.474,0,0,0,6.465-6.465M29.9,5.186V4.274a2.873,2.873,0,0,0-2.8-2.916L27.022.682l.007.675a2.877,2.877,0,0,0-2.8,2.916v.869c.065-.022.136-.043.208-.065a3.963,3.963,0,0,1,.381-.129l.323-.086c.129-.029.259-.065.4-.086s.259-.043.388-.065.23-.036.338-.043a6.637,6.637,0,0,1,.754-.043,6.763,6.763,0,0,1,.761.043c.144.014.28.036.424.057.1.022.208.029.316.05.165.036.33.079.5.122.072.022.144.036.215.057.172.05.338.108.5.172.057.022.108.036.165.057M14.28,27.173v-4.2a7.2,7.2,0,0,1-1.056-.632,9.267,9.267,0,0,0-5.4-1.645,8.809,8.809,0,0,0-6.465,2.593v3.886a7.805,7.805,0,0,1,12.922,0m0-14.467v-.259a6.458,6.458,0,0,0-3.3-5.631,6.546,6.546,0,0,0-6.321,0,6.465,6.465,0,0,0-3.3,5.631v.316a10.542,10.542,0,0,1,6.465-2.119,10.63,10.63,0,0,1,6.192,1.9c.086.057.18.1.266.158m0,18.863a6.461,6.461,0,1,0-6.458,6.465,6.468,6.468,0,0,0,6.458-6.465m-3.62-26.4V4.274a2.873,2.873,0,0,0-2.8-2.916L7.779.682v.675a2.877,2.877,0,0,0-2.8,2.916v.891a1.448,1.448,0,0,1,.165-.05,3.906,3.906,0,0,1,.488-.165l.158-.043a7.89,7.89,0,0,1,2.033-.28,7.878,7.878,0,0,1,2.026.28,1.292,1.292,0,0,1,.158.043,3.906,3.906,0,0,1,.488.165,1.448,1.448,0,0,1,.165.05"/>
|
||||
<path id="패스_1058" data-name="패스 1058" d="M41.539,31.4v3.886a7.812,7.812,0,0,0-12.929,0v-4.2a7.271,7.271,0,0,0,1.063-.632,9.267,9.267,0,0,1,5.4-1.645A8.809,8.809,0,0,1,41.539,31.4" transform="translate(-8.059 -8.116)" fill="#ffae2b"/>
|
||||
<path id="패스_1059" data-name="패스 1059" d="M34.012,19.289v6.86a11.012,11.012,0,0,0-12.664-.2,5.754,5.754,0,0,1-3.4,1.092h-.007a5.786,5.786,0,0,1-3.4-1.092,10.63,10.63,0,0,0-6.192-1.9,10.633,10.633,0,0,0-6.465,2.1V19.289A8.8,8.8,0,0,1,8.355,16.71a9.267,9.267,0,0,1,5.4,1.645,7.191,7.191,0,0,0,8.39,0,9.267,9.267,0,0,1,5.4-1.645,8.81,8.81,0,0,1,6.465,2.579" transform="translate(-0.532 -4.707)" fill="#fff0bf"/>
|
||||
<path id="패스_1060" data-name="패스 1060" d="M41.539,14.8v.316A10.542,10.542,0,0,0,35.075,13a10.652,10.652,0,0,0-6.2,1.9c-.086.057-.18.1-.266.158V14.8a6.48,6.48,0,0,1,3.347-5.66,6.389,6.389,0,0,1,.711-.33.1.1,0,0,1,.043-.014,6.561,6.561,0,0,1,5.524.374,6.465,6.465,0,0,1,3.3,5.631" transform="translate(-8.059 -2.355)" fill="#ffae2b"/>
|
||||
<path id="패스_1061" data-name="패스 1061" d="M35.075,34.95a6.465,6.465,0,1,1-6.465,6.465,6.474,6.474,0,0,1,6.465-6.465m5.086,6.465A5.086,5.086,0,1,0,35.075,46.5a5.095,5.095,0,0,0,5.086-5.086" transform="translate(-8.059 -9.845)" fill="#50b1c2"/>
|
||||
<path id="패스_1062" data-name="패스 1062" d="M35.616,36.87a5.086,5.086,0,1,1-5.086,5.086,5.095,5.095,0,0,1,5.086-5.086m3.728,5.086a3.728,3.728,0,1,0-3.728,3.728,3.734,3.734,0,0,0,3.728-3.728" transform="translate(-8.6 -10.386)"/>
|
||||
<path id="패스_1063" data-name="패스 1063" d="M39.4,4.541v.912c-.057-.022-.108-.036-.165-.057-.165-.065-.33-.122-.5-.172-.072-.022-.144-.036-.215-.057-.172-.043-.338-.086-.5-.122-.108-.022-.215-.029-.316-.05-.144-.022-.28-.043-.424-.057a6.763,6.763,0,0,0-.761-.043,6.637,6.637,0,0,0-.754.043c-.108.007-.223.029-.338.043s-.259.036-.388.065-.266.057-.4.086l-.323.086a3.963,3.963,0,0,0-.381.129c-.072.022-.144.043-.208.065V4.541a2.877,2.877,0,0,1,2.8-2.916L36.524.95l.079.675a2.873,2.873,0,0,1,2.8,2.916" transform="translate(-9.502 -0.268)" fill="#a1f1ff"/>
|
||||
<path id="패스_1064" data-name="패스 1064" d="M36.148,38.76a3.728,3.728,0,1,1-3.728,3.728,3.734,3.734,0,0,1,3.728-3.728M37.7,41.806a.679.679,0,0,0-1-.919l-1.925,2.09a.672.672,0,0,0,.036.955.664.664,0,0,0,.46.18.685.685,0,0,0,.5-.215Z" transform="translate(-9.133 -10.919)" fill="#a1f1ff"/>
|
||||
<path id="패스_1065" data-name="패스 1065" d="M38.509,41.6a.673.673,0,0,1,.043.955l-1.925,2.09a.685.685,0,0,1-.5.215.664.664,0,0,1-.46-.18.672.672,0,0,1-.036-.955l1.925-2.09a.672.672,0,0,1,.955-.036" transform="translate(-9.985 -11.667)"/>
|
||||
<path id="패스_1066" data-name="패스 1066" d="M14.812,31.087v4.2a7.805,7.805,0,0,0-12.922,0V31.4A8.809,8.809,0,0,1,8.355,28.81a9.267,9.267,0,0,1,5.4,1.645,7.2,7.2,0,0,0,1.056.632" transform="translate(-0.532 -8.116)" fill="#ffae2b"/>
|
||||
<path id="패스_1067" data-name="패스 1067" d="M14.812,14.8v.259c-.086-.057-.18-.1-.266-.158A10.63,10.63,0,0,0,8.355,13,10.542,10.542,0,0,0,1.89,15.118V14.8a6.465,6.465,0,0,1,3.3-5.631,6.546,6.546,0,0,1,6.321,0,6.458,6.458,0,0,1,3.3,5.631" transform="translate(-0.532 -2.354)" fill="#ffae2b"/>
|
||||
<path id="패스_1068" data-name="패스 1068" d="M8.355,34.95A6.465,6.465,0,1,1,1.89,41.415,6.468,6.468,0,0,1,8.355,34.95m5.078,6.465A5.082,5.082,0,1,0,8.355,46.5a5.089,5.089,0,0,0,5.078-5.086" transform="translate(-0.532 -9.845)" fill="#50b1c2"/>
|
||||
<path id="패스_1069" data-name="패스 1069" d="M8.9,36.87A5.086,5.086,0,1,1,3.81,41.956,5.089,5.089,0,0,1,8.9,36.87m3.728,5.086A3.728,3.728,0,1,0,8.9,45.684a3.734,3.734,0,0,0,3.728-3.728" transform="translate(-1.073 -10.386)"/>
|
||||
<path id="패스_1070" data-name="패스 1070" d="M9.428,38.76A3.728,3.728,0,1,1,5.7,42.488,3.734,3.734,0,0,1,9.428,38.76m1.552,3.046a.679.679,0,0,0-1-.919l-1.925,2.09a.672.672,0,0,0,.036.955.678.678,0,0,0,.955-.036Z" transform="translate(-1.606 -10.919)" fill="#a1f1ff"/>
|
||||
<path id="패스_1071" data-name="패스 1071" d="M12.612,4.541v.891a1.448,1.448,0,0,0-.165-.05,3.906,3.906,0,0,0-.488-.165,1.292,1.292,0,0,0-.158-.043,7.878,7.878,0,0,0-2.026-.28,7.89,7.89,0,0,0-2.033.28l-.158.043a3.906,3.906,0,0,0-.488.165,1.448,1.448,0,0,0-.165.05V4.541a2.877,2.877,0,0,1,2.8-2.916V.95l.079.675a2.873,2.873,0,0,1,2.8,2.916" transform="translate(-1.952 -0.268)" fill="#a1f1ff"/>
|
||||
<path id="패스_1072" data-name="패스 1072" d="M11.789,41.6a.673.673,0,0,1,.043.955L9.9,44.644a.676.676,0,0,1-.991-.919l1.925-2.09a.672.672,0,0,1,.955-.036" transform="translate(-2.458 -11.667)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,13 @@
|
||||
<svg id="badge_good" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="38.051" height="36.759" viewBox="0 0 38.051 36.759">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_431" data-name="사각형 431" width="38.051" height="36.759" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1655" data-name="그룹 1655" clip-path="url(#clip-path)">
|
||||
<path id="패스_1111" data-name="패스 1111" d="M139.275,21c3.9,0,3.81,5.81,0,5.81h-2.381c3.81.191,3.714,5.905-.1,5.905h-1.715c3.81.19,3.714,5.9-.1,5.9h-2.667c3.81.1,3.714,5.905-.1,5.905H117.941L114.6,42.168V21.4h1.152c4.858-1.333,5.429-7.065,5.525-9.827a5.994,5.994,0,0,1,.095-1.809,6.8,6.8,0,0,1,2.953-.381c3.714.856,3.619,9.714.857,11.618Z" transform="translate(-104.908 -8.561)" fill="#ffd8a8"/>
|
||||
<path id="패스_1112" data-name="패스 1112" d="M18.255,141.172v22.821h-8.9v-23.81h8.9Z" transform="translate(-8.565 -128.33)" fill="#407ed1"/>
|
||||
<path id="패스_1113" data-name="패스 1113" d="M123.661,36.76H109.376a.791.791,0,0,1-.457-.145l-3.343-2.361a.791.791,0,1,1,.913-1.292l3.138,2.216h14.034a2.04,2.04,0,0,0,2.114-2.148,2.017,2.017,0,0,0-2.038-2.175.791.791,0,0,1,.02-1.582h2.667a2.023,2.023,0,0,0,2.113-2.081,2.076,2.076,0,0,0-2.058-2.242.791.791,0,0,1,.04-1.581h1.715a2.023,2.023,0,0,0,2.113-2.081,2.076,2.076,0,0,0-2.058-2.243.791.791,0,0,1,.04-1.581h2.381a1.99,1.99,0,0,0,1.571-.649,2.279,2.279,0,0,0,.531-1.491,1.967,1.967,0,0,0-2.1-2.087h-14.1a.791.791,0,0,1-.449-1.443c1.311-.9,2.021-4.269,1.489-7.06-.159-.831-.675-2.792-2.05-3.131a6.568,6.568,0,0,0-2.094.194c-.009.317-.009.788-.009,1.22,0,.01,0,.018,0,.028a17.2,17.2,0,0,1-1.03,5.71,7.494,7.494,0,0,1-5.075,4.852.785.785,0,0,1-.21.028h-1.152a.791.791,0,0,1,0-1.582h1.042c3.036-.888,4.665-3.932,4.843-9.049,0-.526,0-.98.013-1.313a4.078,4.078,0,0,1,.037-.466.878.878,0,0,1,.516-.738v0a7.528,7.528,0,0,1,3.4-.438l.046.009c1.644.379,2.806,1.935,3.273,4.381a11.653,11.653,0,0,1-.9,7.217h12.4a3.56,3.56,0,0,1,3.684,3.67,3.585,3.585,0,0,1-3.136,3.689,3.946,3.946,0,0,1,.671,2.315,3.63,3.63,0,0,1-2.566,3.469,3.929,3.929,0,0,1,.756,2.434,3.6,3.6,0,0,1-3.39,3.613,3.956,3.956,0,0,1,.629,2.205,3.619,3.619,0,0,1-3.7,3.71M112.879,1.994c-.023,0-.047,0-.071,0,.024,0,.048,0,.071,0" transform="translate(-96.343 0)"/>
|
||||
<path id="패스_1114" data-name="패스 1114" d="M9.69,156.219H.791A.791.791,0,0,1,0,155.428v-23.81a.791.791,0,0,1,.791-.791h8.9a.791.791,0,0,1,.791.791v23.81a.791.791,0,0,1-.791.791m-8.109-1.582H8.9V132.409H1.582Z" transform="translate(0 -119.765)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg id="badge_sincerity" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="65.894" height="44.597" viewBox="0 0 65.894 44.597">
|
||||
<defs>
|
||||
<clipPath id="clip-path">
|
||||
<rect id="사각형_422" data-name="사각형 422" width="60.64" height="26.287" transform="translate(0 0)" fill="none"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g id="그룹_1629" data-name="그룹 1629" transform="translate(0 19.742) rotate(-19)">
|
||||
<g id="그룹_1628" data-name="그룹 1628" transform="translate(0 0)" clip-path="url(#clip-path)">
|
||||
<path id="패스_1082" data-name="패스 1082" d="M169.987,2.233s6.583-1.949,8.555,4.51-4.578,8.519-4.578,8.519Z" transform="translate(-118.876 -1.369)" fill="#ffabbf"/>
|
||||
<path id="패스_1083" data-name="패스 1083" d="M2,66.808l7.865.819L7.892,61.163Z" transform="translate(-1.399 -42.773)" fill="#38322e"/>
|
||||
<path id="패스_1084" data-name="패스 1084" d="M36.043,51.141,33.3,44.854,27.58,42.092,21.6,47.826l1.973,6.464,7.989.831Z" transform="translate(-15.102 -29.436)" fill="#f7dccb"/>
|
||||
<path id="패스_1085" data-name="패스 1085" d="M8.466,66.829a.545.545,0,0,1-.062,0L.539,66.007a.6.6,0,0,1-.354-1.033L6.077,59.33a.6.6,0,0,1,.991.259l1.973,6.464a.6.6,0,0,1-.575.777M1.955,64.945l5.672.59L6.2,60.874Z" transform="translate(0 -41.374)" fill="#231815"/>
|
||||
<path id="패스_1086" data-name="패스 1086" d="M30.158,54.324c-.02,0-.041,0-.062,0l-7.988-.832a.6.6,0,0,1-.513-.422L19.621,46.6a.6.6,0,0,1,.159-.61l5.984-5.734a.6.6,0,0,1,.678-.107l5.716,2.762a.6.6,0,0,1,.289.3L35.195,49.5a.6.6,0,0,1-.152.691l-4.486,3.981a.6.6,0,0,1-.4.152m-7.529-1.99,7.328.763,3.961-3.515L31.435,43.9,26.3,41.417,20.88,46.606Z" transform="translate(-13.703 -28.037)" fill="#231815"/>
|
||||
<path id="패스_1087" data-name="패스 1087" d="M84.109,15.9,45.476,27.7l1.12-3.9-4.733-2.114,2.859-4.027L41.5,14.666,80.132,2.875Z" transform="translate(-29.021 -2.011)" fill="#f78f4a"/>
|
||||
<path id="패스_1088" data-name="패스 1088" d="M44.077,26.9a.6.6,0,0,1-.588-.726l.837-3.95-4.055-1.375a.6.6,0,0,1-.3-.918l2.553-3.6-2.835-2.624a.6.6,0,0,1,.233-1.017L78.558.9a.6.6,0,0,1,.751.4L83.286,14.33a.6.6,0,0,1-.4.751L44.253,26.872a.6.6,0,0,1-.176.026m-2.655-6.93,3.8,1.287a.6.6,0,0,1,.4.694l-.736,3.475L81.96,14.106,78.334,2.227,41.277,13.537l2.455,2.272a.6.6,0,0,1,.082.789Z" transform="translate(-27.623 -0.612)" fill="#231815"/>
|
||||
<path id="패스_1089" data-name="패스 1089" d="M50.819,25.292a.6.6,0,0,1-.175-1.177L87.161,12.969a.6.6,0,1,1,.351,1.15L50.995,25.266a.607.607,0,0,1-.176.026" transform="translate(-35.118 -9.051)" fill="#231815"/>
|
||||
<path id="패스_1090" data-name="패스 1090" d="M56.477,43.829a.6.6,0,0,1-.176-1.177L92.819,31.507a.6.6,0,0,1,.351,1.15L56.653,43.8a.607.607,0,0,1-.176.026" transform="translate(-39.075 -22.015)" fill="#231815"/>
|
||||
<path id="패스_1091" data-name="패스 1091" d="M172.566,14.5a.6.6,0,0,1-.576-.426L168.013,1.04a.6.6,0,0,1,.4-.752c.071-.021,7.18-2.038,9.3,4.912s-4.9,9.246-4.973,9.268a.588.588,0,0,1-.18.028M169.352,1.306l3.6,11.793c1.456-.644,5.076-2.77,3.617-7.548-1.464-4.8-5.65-4.528-7.216-4.244" transform="translate(-117.478 0)" fill="#231815"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="584.988" height="145.001" viewBox="0 0 584.988 145.001">
|
||||
<g id="그룹_6116" data-name="그룹 6116" transform="translate(16262.985 -10083.552)">
|
||||
<path id="교차_6" data-name="교차 6" d="M188.841-1298.9q.035-.663.035-1.336a25.136,25.136,0,0,0-25.108-25.107,25.135,25.135,0,0,0-25.106,25.107q0,.672.035,1.336H101.014q-.014-.667-.014-1.336a62.387,62.387,0,0,1,4.932-24.432,62.846,62.846,0,0,1,5.788-10.662,63.167,63.167,0,0,1,7.664-9.289,63.173,63.173,0,0,1,9.29-7.664,62.75,62.75,0,0,1,10.662-5.788A62.381,62.381,0,0,1,163.768-1363a62.382,62.382,0,0,1,24.432,4.932,62.818,62.818,0,0,1,10.662,5.788,63.181,63.181,0,0,1,9.289,7.664,63.23,63.23,0,0,1,7.665,9.289,62.841,62.841,0,0,1,5.787,10.662,62.372,62.372,0,0,1,4.933,24.432q0,.669-.014,1.336Z" transform="translate(-16363.985 11527.449)" fill="#fff" opacity="0.1"/>
|
||||
<path id="교차_7" data-name="교차 7" d="M250.96-1259.9a42.993,42.993,0,0,0-42.691-38.645,42.993,42.993,0,0,0-42.692,38.645H101.084a106.683,106.683,0,0,1,8.345-37.49,107.487,107.487,0,0,1,9.89-18.249,108.16,108.16,0,0,1,13.1-15.9,108.074,108.074,0,0,1,15.875-13.118,107.215,107.215,0,0,1,18.221-9.905A106.466,106.466,0,0,1,208.268-1363a106.462,106.462,0,0,1,41.753,8.443,107.215,107.215,0,0,1,18.221,9.905,108.035,108.035,0,0,1,15.875,13.118,108.16,108.16,0,0,1,13.1,15.9,107.551,107.551,0,0,1,9.89,18.249,106.683,106.683,0,0,1,8.345,37.49Z" transform="translate(-15576.913 8823.656) rotate(-180)" fill="#fff" opacity="0.1"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg id="eraser" xmlns="http://www.w3.org/2000/svg" width="125.5" height="29" viewBox="0 0 125.5 29">
|
||||
<g id="그룹_4510" data-name="그룹 4510" transform="translate(-2337 -6920)">
|
||||
<g id="사각형_599" data-name="사각형 599" transform="translate(2337 6920)" fill="#54b175" stroke="#263b3a" stroke-width="3">
|
||||
<rect width="61" height="29" rx="5" stroke="none"/>
|
||||
<rect x="1.5" y="1.5" width="58" height="26" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
<g id="사각형_600" data-name="사각형 600" transform="translate(2363 6921)" fill="#fff" stroke="#263b3a" stroke-width="3">
|
||||
<rect width="10" height="27" rx="5" stroke="none"/>
|
||||
<rect x="1.5" y="1.5" width="7" height="24" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="사각형_1680" data-name="사각형 1680" transform="translate(125.5 19) rotate(90)" fill="#fff" stroke="#263b3a" stroke-width="3">
|
||||
<rect width="10" height="44" rx="5" stroke="none"/>
|
||||
<rect x="1.5" y="1.5" width="7" height="41" rx="3.5" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1022 B |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
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 |