diff --git a/ReadMe.md b/ReadMe.md index fa1c7e4..631addd 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -14,6 +14,7 @@ ### 구현 현황 - [x] 개발 환경 구축 - [x] DB 설계 및 Entity 구현 +- [x] HTTP(HTTPS) URL 검증 구현 - [x] URL 단축 알고리즘 구현 - [ ] URL 단축 저장 및 조회 기능 구현 - [ ] 테스트 및 배포 \ No newline at end of file diff --git a/sql/DDL.sql b/sql/DDL.sql index 732b0ef..08c64c0 100644 --- a/sql/DDL.sql +++ b/sql/DDL.sql @@ -6,7 +6,7 @@ DROP TABLE IF EXISTS url_map; DROP TABLE IF EXISTS users; CREATE TABLE `users`( - `user_id` INT NOT NULL AUTO_INCREMENT, + `user_id` BIGINT NOT NULL AUTO_INCREMENT, `user_email` VARCHAR(100) NOT NULL, `user_password` VARCHAR(30) NOT NULL, `user_nickname` VARCHAR(20) NOT NULL, @@ -15,14 +15,14 @@ CREATE TABLE `users`( ); CREATE TABLE `url_map`( - `url_map_id` INT NOT NULL AUTO_INCREMENT, + `url_map_id` BIGINT NOT NULL AUTO_INCREMENT, `url_map_original` VARCHAR(255) NOT NULL, - `url_map_short` VARCHAR(7) NOT NULL, + `url_map_short` VARCHAR(20) NOT NULL, `url_map_is_active` TINYINT(1) NOT NULL, - `user_id` INT NULL, - `url_map_click` INT NOT NULL DEFAULT 0, + `user_id` BIGINT NULL, + `url_map_click` BIGINT NOT NULL DEFAULT 0, `url_map_created_at` DATETIME NOT NULL, - `url_map_edited_at` DATETIME NOT NULL, + `url_map_updated_at` DATETIME NOT NULL, `url_map_expired_at` DATETIME NULL, PRIMARY KEY(url_map_id), FOREIGN KEY(user_id) REFERENCES users(user_id) diff --git a/src/main/java/be/gyu/urlShortener/entity/UrlMap.java b/src/main/java/be/gyu/urlShortener/entity/UrlMap.java index 0fcb4cc..a746754 100644 --- a/src/main/java/be/gyu/urlShortener/entity/UrlMap.java +++ b/src/main/java/be/gyu/urlShortener/entity/UrlMap.java @@ -2,28 +2,36 @@ package be.gyu.urlShortener.entity; import java.time.LocalDateTime; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter +@Builder(toBuilder=true) +@NoArgsConstructor +@AllArgsConstructor public class UrlMap { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) - private int urlMapId; + private Long urlMapId; private String urlMapOriginal; private String urlMapShort; private boolean urlMapIsActive; @ManyToOne @JoinColumn(name="user_id") private Users user; - private int urlMapClick; + @Column(insertable=false) + private Long urlMapClick; private LocalDateTime urlMapCreatedAt; - private LocalDateTime urlMapEditedAt; + private LocalDateTime urlMapUpdatedAt; private LocalDateTime urlMapExpiredAt; } diff --git a/src/main/java/be/gyu/urlShortener/entity/Users.java b/src/main/java/be/gyu/urlShortener/entity/Users.java index d257e60..e774dd0 100644 --- a/src/main/java/be/gyu/urlShortener/entity/Users.java +++ b/src/main/java/be/gyu/urlShortener/entity/Users.java @@ -6,14 +6,16 @@ import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import lombok.Builder; import lombok.Getter; @Entity @Getter +@Builder public class Users { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) - private int userId; + private Long userId; private String userEmail; private String userPassword; private String userNickname; diff --git a/src/main/java/be/gyu/urlShortener/exception/ShortUrlNotFoundException.java b/src/main/java/be/gyu/urlShortener/exception/ShortUrlNotFoundException.java new file mode 100644 index 0000000..b5bd276 --- /dev/null +++ b/src/main/java/be/gyu/urlShortener/exception/ShortUrlNotFoundException.java @@ -0,0 +1,6 @@ +package be.gyu.urlShortener.exception; + +public class ShortUrlNotFoundException extends RuntimeException{ + public ShortUrlNotFoundException(){ super("존재하지 않는 단축 URL 입니다."); } + public ShortUrlNotFoundException(String msg){ super(msg); } +} diff --git a/src/main/java/be/gyu/urlShortener/repository/UrlMapRepository.java b/src/main/java/be/gyu/urlShortener/repository/UrlMapRepository.java new file mode 100644 index 0000000..c741119 --- /dev/null +++ b/src/main/java/be/gyu/urlShortener/repository/UrlMapRepository.java @@ -0,0 +1,11 @@ +package be.gyu.urlShortener.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import be.gyu.urlShortener.entity.UrlMap; + +public interface UrlMapRepository extends JpaRepository { + public Optional findByUrlMapShort(String urlMapShort); +} \ No newline at end of file diff --git a/src/main/java/be/gyu/urlShortener/service/MainService.java b/src/main/java/be/gyu/urlShortener/service/MainService.java new file mode 100644 index 0000000..72b9b40 --- /dev/null +++ b/src/main/java/be/gyu/urlShortener/service/MainService.java @@ -0,0 +1,95 @@ +package be.gyu.urlShortener.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import be.gyu.urlShortener.entity.UrlMap; +import be.gyu.urlShortener.exception.ShortUrlNotFoundException; +import be.gyu.urlShortener.repository.UrlMapRepository; + +import java.util.Optional; +import java.time.LocalDateTime; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import io.seruco.encoding.base62.Base62; + +@Service +public class MainService { + @Autowired + private UrlMapRepository urlMapRepository; + + // HTTP(S) URL 검증 패턴식 + private final String urlRegPattern="^((http|https):\\/\\/)?([a-z0-9-]{2,}\\.[a-z]{2,}|([0-9]{1,3}\\.){3}[0-9]{1,3})[\\w.\\/가-힣\\-\\ ?=&:]*"; + // Base62 Encoder Instance 생성 및 호출 + private Base62 base62=Base62.createInstance(); + + // HTTP(S) URL 검증 메소드 + public boolean validOriginalUrl(String url){ + return url.matches(urlRegPattern); + } + + // URL 단축 메소드 + public String createUrlShort(String url){ + // SHA-256 Hashing을 위해 Instance 생성 및 호출 + MessageDigest md; + try{ + md=MessageDigest.getInstance("sha256"); + }catch(NoSuchAlgorithmException e){ + e.printStackTrace(); + throw new RuntimeException(e); + } + + // DB에 저장하기 위해 Entity 객체 정의 + UrlMap urlMap=UrlMap.builder() + .urlMapOriginal(url) + .urlMapShort("") + .urlMapIsActive(false) + .urlMapClick(0l) + .urlMapCreatedAt(LocalDateTime.now()) + .urlMapUpdatedAt(LocalDateTime.now()) + .build(); + + // DB에 저장 - 초기 저장 (단축 URL은 빈 값으로 우선 저장) + urlMapRepository.save(urlMap); + + // 고유 값 생성 + // 고유한 값은 DB의 ID 값 + 현재 시간을 결합한 문자열을 SHA-256으로 해싱한 후, + // Base62로 인코딩 진행 + long id=urlMap.getUrlMapId(); + String urlMapIdString=String.format("%011d",id); + String nowDateTimeString=LocalDateTime.now().toString(); + String result=urlMapIdString+nowDateTimeString; + + // Base62 Encoding + byte[] barr=base62.encode(md.digest(result.getBytes())); + StringBuilder sb=new StringBuilder(); + for(byte b : barr){ + sb.append(String.format("%c",b)); + } + + // 해싱 및 인코딩된 문장을 7자리 추출하여 저장 + String shortResult=sb.toString().substring(0,7); + urlMap=urlMap.toBuilder() + .urlMapShort(shortResult) + .build(); + + // 최종 저장 - 단축된 URL도 포함하여 저장 + // (기존 데이터에 Update하는 방법으로 저장) + urlMapRepository.save(urlMap); + + // 단축된 URL 반환 + return shortResult; + } + + // 단축 URL로 원본 URL 조회 및 반환 메소드 + public String getOriginalUrl(String shortUrl){ + Optional optional=urlMapRepository.findByUrlMapShort(shortUrl); + + if(!optional.isPresent()){ + throw new ShortUrlNotFoundException(); + } + + return optional.get().getUrlMapOriginal(); + } +}