diff --git a/build.gradle b/build.gradle index a61b108..fdf4bb0 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,9 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5' // JSON 파싱용 + // 게시판 스크래핑용 HTML 파서 + implementation 'org.jsoup:jsoup:1.18.1' + } tasks.named('test') { diff --git a/src/main/java/company/specialsource/SpecialsourceHomepageApplication.java b/src/main/java/company/specialsource/SpecialsourceHomepageApplication.java index a633f36..4cc3cbf 100644 --- a/src/main/java/company/specialsource/SpecialsourceHomepageApplication.java +++ b/src/main/java/company/specialsource/SpecialsourceHomepageApplication.java @@ -2,8 +2,10 @@ package company.specialsource; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class SpecialsourceHomepageApplication { public static void main(String[] args) { diff --git a/src/main/java/company/specialsource/config/DataInitializer.java b/src/main/java/company/specialsource/config/DataInitializer.java index f8361a4..766a6a1 100644 --- a/src/main/java/company/specialsource/config/DataInitializer.java +++ b/src/main/java/company/specialsource/config/DataInitializer.java @@ -21,7 +21,7 @@ public class DataInitializer implements CommandLineRunner { if (userRepository.findByUsername("admin").isEmpty()) { userRepository.save(User.builder() .username("admin") - .password(passwordEncoder.encode("1234")) + .password(passwordEncoder.encode("Ppasse444$")) .role("ROLE_USER") .build()); log.info("초기 관리자 계정(admin)이 생성되었습니다."); diff --git a/src/main/java/company/specialsource/config/JwtAuthenticationFilter.java b/src/main/java/company/specialsource/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..19dbd2a --- /dev/null +++ b/src/main/java/company/specialsource/config/JwtAuthenticationFilter.java @@ -0,0 +1,46 @@ +package company.specialsource.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final String HEADER_PREFIX = "Bearer "; + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + String header = request.getHeader("Authorization"); + + if (header != null && header.startsWith(HEADER_PREFIX)) { + String token = header.substring(HEADER_PREFIX.length()); + + if (jwtTokenProvider.validateToken(token)) { + String username = jwtTokenProvider.getUsername(token); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(username, null, List.of()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/company/specialsource/config/SecurityConfig.java b/src/main/java/company/specialsource/config/SecurityConfig.java index f07efe9..1cbc365 100644 --- a/src/main/java/company/specialsource/config/SecurityConfig.java +++ b/src/main/java/company/specialsource/config/SecurityConfig.java @@ -1,25 +1,33 @@ package company.specialsource.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // REST API 통신을 위해 CSRF 비활성화 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/login").permitAll() // 로그인 API는 누구나 접근 가능 .anyRequest().authenticated() - ); + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/src/main/java/company/specialsource/controller/ScrapedPostController.java b/src/main/java/company/specialsource/controller/ScrapedPostController.java new file mode 100644 index 0000000..a876368 --- /dev/null +++ b/src/main/java/company/specialsource/controller/ScrapedPostController.java @@ -0,0 +1,56 @@ +package company.specialsource.controller; + +import company.specialsource.scraper.BoardSource; +import company.specialsource.scraper.BoardSummaryResponse; +import company.specialsource.scraper.ScrapedPostQueryService; +import company.specialsource.scraper.ScrapedPostResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; +import java.util.List; + +@RestController +@RequestMapping("/api/scraped-posts") +@RequiredArgsConstructor +public class ScrapedPostController { + + private final ScrapedPostQueryService scrapedPostQueryService; + + @GetMapping + public ResponseEntity getPosts( + @RequestParam(required = false) String board, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size + ) { + if (board != null && !isValidBoardKey(board)) { + return ResponseEntity.badRequest().body("존재하지 않는 게시판 키입니다: " + board); + } + + Pageable pageable = PageRequest.of(page, Math.min(size, 100)); + Page posts = scrapedPostQueryService.getPosts(board, pageable); + return ResponseEntity.ok(posts); + } + + @GetMapping("/boards") + public ResponseEntity> getBoards() { + List boards = Arrays.stream(BoardSource.values()) + .map(BoardSummaryResponse::from) + .toList(); + return ResponseEntity.ok(boards); + } + + @PostMapping("/{id}/view") + public ResponseEntity markViewed(@PathVariable Long id) { + scrapedPostQueryService.markViewed(id); + return ResponseEntity.noContent().build(); + } + + private boolean isValidBoardKey(String boardKey) { + return Arrays.stream(BoardSource.values()).anyMatch(source -> source.name().equals(boardKey)); + } +} diff --git a/src/main/java/company/specialsource/entity/ScrapedPost.java b/src/main/java/company/specialsource/entity/ScrapedPost.java new file mode 100644 index 0000000..76d5fbe --- /dev/null +++ b/src/main/java/company/specialsource/entity/ScrapedPost.java @@ -0,0 +1,56 @@ +package company.specialsource.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "scraped_posts", + uniqueConstraints = @UniqueConstraint(columnNames = {"board_key", "link"})) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class ScrapedPost { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "board_key", nullable = false) + private String boardKey; // BoardSource enum name, 예: RULIWEB_BEST + + @Column(name = "site_name", nullable = false) + private String siteName; // 예: 루리웹, 디씨인사이드 + + @Column(name = "board_name", nullable = false) + private String boardName; // 예: 실시간베스트, 주짓수 갤러리 + + @Column(nullable = false, length = 500) + private String title; + + @Column(nullable = false, length = 1000) + private String link; + + private String author; + + private String postedAt; // 사이트마다 표기 형식이 달라(HH:mm, yy.MM.dd 등) 원문 그대로 저장 + + private Integer viewCount; + + private Integer likeCount; + + @Column(columnDefinition = "TEXT") + private String content; // 게시물 상세페이지에서 추출한 본문 (정제된 HTML, 원본 문단/링크/이미지 위치 보존) + + @Column(name = "image_urls", columnDefinition = "TEXT") + private String imageUrls; // 본문에 포함된 이미지 URL, 줄바꿈으로 구분 + + @Column(name = "scraped_at", nullable = false) + private LocalDateTime scrapedAt; + + @Setter + @Column(name = "viewed_at") + private LocalDateTime viewedAt; // 게시판 조회 시각. null이면 아직 안 읽은 글 +} diff --git a/src/main/java/company/specialsource/repository/ScrapedPostRepository.java b/src/main/java/company/specialsource/repository/ScrapedPostRepository.java new file mode 100644 index 0000000..d45fcef --- /dev/null +++ b/src/main/java/company/specialsource/repository/ScrapedPostRepository.java @@ -0,0 +1,20 @@ +package company.specialsource.repository; + +import company.specialsource.entity.ScrapedPost; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +public interface ScrapedPostRepository extends JpaRepository { + boolean existsByBoardKeyAndLink(String boardKey, String link); + + Page findByBoardKeyAndViewedAtIsNullOrderByScrapedAtDesc(String boardKey, Pageable pageable); + + Page findByViewedAtIsNullOrderByScrapedAtDesc(Pageable pageable); + + @Transactional + long deleteByScrapedAtBefore(LocalDateTime cutoff); +} diff --git a/src/main/java/company/specialsource/scraper/BoardParser.java b/src/main/java/company/specialsource/scraper/BoardParser.java new file mode 100644 index 0000000..e18cb1b --- /dev/null +++ b/src/main/java/company/specialsource/scraper/BoardParser.java @@ -0,0 +1,9 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; + +import java.util.List; + +public interface BoardParser { + List parse(Document document); +} diff --git a/src/main/java/company/specialsource/scraper/BoardScraperService.java b/src/main/java/company/specialsource/scraper/BoardScraperService.java new file mode 100644 index 0000000..2a76a86 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/BoardScraperService.java @@ -0,0 +1,37 @@ +package company.specialsource.scraper; + +import lombok.RequiredArgsConstructor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class BoardScraperService { + + private static final String USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"; + private static final int TIMEOUT_MS = 10_000; + + private final RuliwebBoardListParser ruliwebBoardListParser; + private final RuliwebMypiParser ruliwebMypiParser; + private final DcGalleryParser dcGalleryParser; + + public List scrape(BoardSource source) throws IOException { + Document document = Jsoup.connect(source.getUrl()) + .userAgent(USER_AGENT) + .timeout(TIMEOUT_MS) + .get(); + + BoardParser parser = switch (source.getParserType()) { + case RULIWEB_BOARD_LIST -> ruliwebBoardListParser; + case RULIWEB_MYPI_BEST -> ruliwebMypiParser; + case DC_GALLERY_LIST -> dcGalleryParser; + }; + + return parser.parse(document); + } +} diff --git a/src/main/java/company/specialsource/scraper/BoardSource.java b/src/main/java/company/specialsource/scraper/BoardSource.java new file mode 100644 index 0000000..fe3b738 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/BoardSource.java @@ -0,0 +1,27 @@ +package company.specialsource.scraper; + +import lombok.Getter; + +@Getter +public enum BoardSource { + RULIWEB_BEST("루리웹", "실시간베스트", "https://bbs.ruliweb.com/best/selection", ParserType.RULIWEB_BOARD_LIST, DetailParserType.RULIWEB), + RULIWEB_HUMOR("루리웹", "유머", "https://bbs.ruliweb.com/best/humor", ParserType.RULIWEB_BOARD_LIST, DetailParserType.RULIWEB), + RULIWEB_MYPI("루리웹", "마이피", "https://mypi.ruliweb.com/", ParserType.RULIWEB_MYPI_BEST, DetailParserType.RULIWEB), + RULIWEB_HOTDEAL("루리웹", "핫딜", "https://bbs.ruliweb.com/market/board/1020", ParserType.RULIWEB_BOARD_LIST, DetailParserType.RULIWEB), + DC_BJJ("디씨인사이드", "주짓수 갤러리", "https://gall.dcinside.com/mgallery/board/lists/?id=bjj", ParserType.DC_GALLERY_LIST, DetailParserType.DC), + DC_SINGULARITY("디씨인사이드", "특이점이 온다", "https://gall.dcinside.com/mgallery/board/lists?id=thesingularity", ParserType.DC_GALLERY_LIST, DetailParserType.DC); + + private final String siteName; + private final String boardName; + private final String url; + private final ParserType parserType; + private final DetailParserType detailParserType; + + BoardSource(String siteName, String boardName, String url, ParserType parserType, DetailParserType detailParserType) { + this.siteName = siteName; + this.boardName = boardName; + this.url = url; + this.parserType = parserType; + this.detailParserType = detailParserType; + } +} diff --git a/src/main/java/company/specialsource/scraper/BoardSummaryResponse.java b/src/main/java/company/specialsource/scraper/BoardSummaryResponse.java new file mode 100644 index 0000000..9c9fdfa --- /dev/null +++ b/src/main/java/company/specialsource/scraper/BoardSummaryResponse.java @@ -0,0 +1,7 @@ +package company.specialsource.scraper; + +public record BoardSummaryResponse(String boardKey, String siteName, String boardName) { + public static BoardSummaryResponse from(BoardSource source) { + return new BoardSummaryResponse(source.name(), source.getSiteName(), source.getBoardName()); + } +} diff --git a/src/main/java/company/specialsource/scraper/DcGalleryParser.java b/src/main/java/company/specialsource/scraper/DcGalleryParser.java new file mode 100644 index 0000000..2e2a134 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/DcGalleryParser.java @@ -0,0 +1,53 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 디씨인사이드 갤러리 목록 파서. 공지/설문 행은 제외하고 일반 게시글만 수집한다. + */ +@Component +public class DcGalleryParser implements BoardParser { + + @Override + public List parse(Document document) { + List posts = new ArrayList<>(); + + for (Element row : document.select("tr.ub-content")) { + if (!row.hasClass("us-post") || "icon_notice".equals(row.attr("data-type"))) { + continue; + } + + Element titleAnchor = row.selectFirst("td.gall_tit a"); + if (titleAnchor == null) { + continue; + } + + String title = titleAnchor.text().trim(); + String link = titleAnchor.attr("abs:href"); + if (title.isEmpty() || link.isEmpty()) { + continue; + } + + Element writerEl = row.selectFirst("td.gall_writer"); + String author = writerEl == null ? "" : writerEl.attr("data-nick"); + if (author.isBlank() && writerEl != null) { + author = writerEl.text().trim(); + } + + Element dateEl = row.selectFirst("td.gall_date"); + String postedAt = dateEl == null ? "" : dateEl.hasAttr("title") ? dateEl.attr("title") : dateEl.text().trim(); + + Integer viewCount = ScraperTextUtils.parseIntSafe(row.select("td.gall_count").text()); + Integer likeCount = ScraperTextUtils.parseIntSafe(row.select("td.gall_recommend").text()); + + posts.add(new ScrapedPostDto(title, link, author, postedAt, viewCount, likeCount)); + } + + return posts; + } +} diff --git a/src/main/java/company/specialsource/scraper/DcPostDetailParser.java b/src/main/java/company/specialsource/scraper/DcPostDetailParser.java new file mode 100644 index 0000000..ab1c2e2 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/DcPostDetailParser.java @@ -0,0 +1,13 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; +import org.springframework.stereotype.Component; + +@Component +public class DcPostDetailParser implements DetailParser { + + @Override + public PostDetail parse(Document document) { + return ScraperTextUtils.extractDetail(document.selectFirst("div.write_div")); + } +} diff --git a/src/main/java/company/specialsource/scraper/DetailParser.java b/src/main/java/company/specialsource/scraper/DetailParser.java new file mode 100644 index 0000000..11734c1 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/DetailParser.java @@ -0,0 +1,7 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; + +public interface DetailParser { + PostDetail parse(Document document); +} diff --git a/src/main/java/company/specialsource/scraper/DetailParserType.java b/src/main/java/company/specialsource/scraper/DetailParserType.java new file mode 100644 index 0000000..433c7a8 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/DetailParserType.java @@ -0,0 +1,6 @@ +package company.specialsource.scraper; + +public enum DetailParserType { + RULIWEB, + DC +} diff --git a/src/main/java/company/specialsource/scraper/ParserType.java b/src/main/java/company/specialsource/scraper/ParserType.java new file mode 100644 index 0000000..f08504e --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ParserType.java @@ -0,0 +1,7 @@ +package company.specialsource.scraper; + +public enum ParserType { + RULIWEB_BOARD_LIST, // 표 형태 게시판 (실시간베스트, 유머, 핫딜) + RULIWEB_MYPI_BEST, // 마이피 실시간 인기글 위젯 + DC_GALLERY_LIST // 디씨인사이드 갤러리 게시판 +} diff --git a/src/main/java/company/specialsource/scraper/PostDetail.java b/src/main/java/company/specialsource/scraper/PostDetail.java new file mode 100644 index 0000000..e345f97 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/PostDetail.java @@ -0,0 +1,10 @@ +package company.specialsource.scraper; + +import java.util.List; + +/** + * content는 원본 게시물의 문단/굵게/링크/이미지 위치를 보존한 정제된(sanitized) HTML이다. + */ +public record PostDetail(String content, List imageUrls) { + static final PostDetail EMPTY = new PostDetail("", List.of()); +} diff --git a/src/main/java/company/specialsource/scraper/PostDetailScraperService.java b/src/main/java/company/specialsource/scraper/PostDetailScraperService.java new file mode 100644 index 0000000..17af047 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/PostDetailScraperService.java @@ -0,0 +1,34 @@ +package company.specialsource.scraper; + +import lombok.RequiredArgsConstructor; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.springframework.stereotype.Service; + +import java.io.IOException; + +@Service +@RequiredArgsConstructor +public class PostDetailScraperService { + + private static final String USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36"; + private static final int TIMEOUT_MS = 10_000; + + private final RuliwebPostDetailParser ruliwebPostDetailParser; + private final DcPostDetailParser dcPostDetailParser; + + public PostDetail scrape(String url, DetailParserType type) throws IOException { + Document document = Jsoup.connect(url) + .userAgent(USER_AGENT) + .timeout(TIMEOUT_MS) + .get(); + + DetailParser parser = switch (type) { + case RULIWEB -> ruliwebPostDetailParser; + case DC -> dcPostDetailParser; + }; + + return parser.parse(document); + } +} diff --git a/src/main/java/company/specialsource/scraper/RuliwebBoardListParser.java b/src/main/java/company/specialsource/scraper/RuliwebBoardListParser.java new file mode 100644 index 0000000..9157b79 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/RuliwebBoardListParser.java @@ -0,0 +1,48 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 루리웹 표 형태 게시판(실시간베스트, 유머, 핫딜) 공통 파서. + * 게시판마다 tr.table_body 구조는 같고, 공지(class=notice) 행만 다르게 섞여 있어 걸러낸다. + */ +@Component +public class RuliwebBoardListParser implements BoardParser { + + @Override + public List parse(Document document) { + List posts = new ArrayList<>(); + + for (Element row : document.select("tr.table_body")) { + if (row.hasClass("notice")) { + continue; + } + + Element titleAnchor = row.selectFirst("td.subject a"); + if (titleAnchor == null) { + continue; + } + + Element strong = titleAnchor.selectFirst("strong"); + String title = (strong != null ? strong.text() : titleAnchor.text()).trim(); + String link = titleAnchor.attr("abs:href"); + if (title.isEmpty() || link.isEmpty()) { + continue; + } + + String author = row.select("td.writer").text().trim(); + String postedAt = row.select("td.time").text().trim(); + Integer likeCount = ScraperTextUtils.parseIntSafe(row.select("td.recomd").text()); + Integer viewCount = ScraperTextUtils.parseIntSafe(row.select("td.hit").text()); + + posts.add(new ScrapedPostDto(title, link, author, postedAt, viewCount, likeCount)); + } + + return posts; + } +} diff --git a/src/main/java/company/specialsource/scraper/RuliwebMypiParser.java b/src/main/java/company/specialsource/scraper/RuliwebMypiParser.java new file mode 100644 index 0000000..a571a62 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/RuliwebMypiParser.java @@ -0,0 +1,36 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +/** + * 마이피(mypi.ruliweb.com)의 "실시간 인기글" 위젯 파서. + * 작성자/날짜/조회수는 목록에 노출되지 않아 title, link만 수집한다. + */ +@Component +public class RuliwebMypiParser implements BoardParser { + + @Override + public List parse(Document document) { + List posts = new ArrayList<>(); + + for (Element anchor : document.select("ul.right_best_list li.right_best_list_item a.txt_link")) { + String title = anchor.text().trim(); + String link = anchor.attr("abs:href"); + if (title.isEmpty() || link.isEmpty()) { + continue; + } + // 위젯 목록에 광고(외부 리다이렉트 링크)가 섞여 들어오는 경우가 있어 루리웹 도메인만 통과시킨다. + if (!link.contains("ruliweb.com")) { + continue; + } + posts.add(new ScrapedPostDto(title, link, null, null, null, null)); + } + + return posts; + } +} diff --git a/src/main/java/company/specialsource/scraper/RuliwebPostDetailParser.java b/src/main/java/company/specialsource/scraper/RuliwebPostDetailParser.java new file mode 100644 index 0000000..36f57bf --- /dev/null +++ b/src/main/java/company/specialsource/scraper/RuliwebPostDetailParser.java @@ -0,0 +1,13 @@ +package company.specialsource.scraper; + +import org.jsoup.nodes.Document; +import org.springframework.stereotype.Component; + +@Component +public class RuliwebPostDetailParser implements DetailParser { + + @Override + public PostDetail parse(Document document) { + return ScraperTextUtils.extractDetail(document.selectFirst("div.view_content")); + } +} diff --git a/src/main/java/company/specialsource/scraper/ScrapedPostCleanupScheduler.java b/src/main/java/company/specialsource/scraper/ScrapedPostCleanupScheduler.java new file mode 100644 index 0000000..bc54545 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScrapedPostCleanupScheduler.java @@ -0,0 +1,37 @@ +package company.specialsource.scraper; + +import company.specialsource.repository.ScrapedPostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +/** + * 오래된 스크랩 게시물을 주기적으로 정리한다. + * 스크래핑으로 계속 쌓이기만 하는 캐시성 데이터라 일정 기간이 지나면 삭제해도 무방하다. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ScrapedPostCleanupScheduler { + + private final ScrapedPostRepository scrapedPostRepository; + + @Value("${scraping.retention-days:7}") + private int retentionDays; + + @Scheduled( + initialDelayString = "${scraping.cleanup-initial-delay-ms:60000}", + fixedDelayString = "${scraping.cleanup-interval-ms:86400000}" + ) + public void deleteExpiredPosts() { + LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays); + long deletedCount = scrapedPostRepository.deleteByScrapedAtBefore(cutoff); + if (deletedCount > 0) { + log.info("[정리] 스크랩 {}일 경과 게시물 {}건 삭제 (기준: {})", retentionDays, deletedCount, cutoff); + } + } +} diff --git a/src/main/java/company/specialsource/scraper/ScrapedPostDto.java b/src/main/java/company/specialsource/scraper/ScrapedPostDto.java new file mode 100644 index 0000000..855937d --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScrapedPostDto.java @@ -0,0 +1,11 @@ +package company.specialsource.scraper; + +public record ScrapedPostDto( + String title, + String link, + String author, + String postedAt, + Integer viewCount, + Integer likeCount +) { +} diff --git a/src/main/java/company/specialsource/scraper/ScrapedPostQueryService.java b/src/main/java/company/specialsource/scraper/ScrapedPostQueryService.java new file mode 100644 index 0000000..47191e2 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScrapedPostQueryService.java @@ -0,0 +1,35 @@ +package company.specialsource.scraper; + +import company.specialsource.entity.ScrapedPost; +import company.specialsource.repository.ScrapedPostRepository; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +public class ScrapedPostQueryService { + + private final ScrapedPostRepository scrapedPostRepository; + + public Page getPosts(String boardKey, Pageable pageable) { + // 이미 조회한(viewedAt != null) 글은 목록에서 제외한다. + Page page = (boardKey == null || boardKey.isBlank()) + ? scrapedPostRepository.findByViewedAtIsNullOrderByScrapedAtDesc(pageable) + : scrapedPostRepository.findByBoardKeyAndViewedAtIsNullOrderByScrapedAtDesc(boardKey, pageable); + + return page.map(ScrapedPostResponse::from); + } + + @Transactional + public void markViewed(Long id) { + ScrapedPost post = scrapedPostRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("게시물을 찾을 수 없습니다: " + id)); + post.setViewedAt(LocalDateTime.now()); + } +} diff --git a/src/main/java/company/specialsource/scraper/ScrapedPostResponse.java b/src/main/java/company/specialsource/scraper/ScrapedPostResponse.java new file mode 100644 index 0000000..37a8c3e --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScrapedPostResponse.java @@ -0,0 +1,44 @@ +package company.specialsource.scraper; + +import company.specialsource.entity.ScrapedPost; + +import java.time.LocalDateTime; +import java.util.List; + +public record ScrapedPostResponse( + Long id, + String boardKey, + String siteName, + String boardName, + String title, + String link, + String author, + String postedAt, + Integer viewCount, + Integer likeCount, + String content, + List imageUrls, + LocalDateTime scrapedAt +) { + public static ScrapedPostResponse from(ScrapedPost post) { + List imageUrls = (post.getImageUrls() == null || post.getImageUrls().isBlank()) + ? List.of() + : List.of(post.getImageUrls().split("\n")); + + return new ScrapedPostResponse( + post.getId(), + post.getBoardKey(), + post.getSiteName(), + post.getBoardName(), + post.getTitle(), + post.getLink(), + post.getAuthor(), + post.getPostedAt(), + post.getViewCount(), + post.getLikeCount(), + post.getContent(), + imageUrls, + post.getScrapedAt() + ); + } +} diff --git a/src/main/java/company/specialsource/scraper/ScrapedPostSyncService.java b/src/main/java/company/specialsource/scraper/ScrapedPostSyncService.java new file mode 100644 index 0000000..8382702 --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScrapedPostSyncService.java @@ -0,0 +1,72 @@ +package company.specialsource.scraper; + +import company.specialsource.entity.ScrapedPost; +import company.specialsource.repository.ScrapedPostRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 목록에 새로 나타난 글만 상세페이지까지 들어가서 본문을 수집한다. + * 이미 저장된 글은 상세 요청을 다시 하지 않으므로 매 주기마다 요청량이 쌓이지 않는다. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ScrapedPostSyncService { + + private static final long DELAY_BETWEEN_DETAIL_FETCHES_MS = 800; + + private final ScrapedPostRepository scrapedPostRepository; + private final PostDetailScraperService postDetailScraperService; + + public int saveNewPosts(BoardSource source, List posts) { + int savedCount = 0; + for (ScrapedPostDto post : posts) { + if (scrapedPostRepository.existsByBoardKeyAndLink(source.name(), post.link())) { + continue; + } + + PostDetail detail = fetchDetailSafely(source, post); + + scrapedPostRepository.save(ScrapedPost.builder() + .boardKey(source.name()) + .siteName(source.getSiteName()) + .boardName(source.getBoardName()) + .title(post.title()) + .link(post.link()) + .author(post.author()) + .postedAt(post.postedAt()) + .viewCount(post.viewCount()) + .likeCount(post.likeCount()) + .content(detail.content()) + .imageUrls(String.join("\n", detail.imageUrls())) + .scrapedAt(LocalDateTime.now()) + .build()); + savedCount++; + + sleepQuietly(); + } + return savedCount; + } + + private PostDetail fetchDetailSafely(BoardSource source, ScrapedPostDto post) { + try { + return postDetailScraperService.scrape(post.link(), source.getDetailParserType()); + } catch (Exception e) { + log.warn("[본문 스크래핑 실패] {} - {}", post.link(), e.getMessage()); + return PostDetail.EMPTY; + } + } + + private void sleepQuietly() { + try { + Thread.sleep(DELAY_BETWEEN_DETAIL_FETCHES_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/java/company/specialsource/scraper/ScraperTextUtils.java b/src/main/java/company/specialsource/scraper/ScraperTextUtils.java new file mode 100644 index 0000000..f56164f --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScraperTextUtils.java @@ -0,0 +1,73 @@ +package company.specialsource.scraper; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Element; +import org.jsoup.safety.Safelist; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +final class ScraperTextUtils { + + private static final Safelist CONTENT_SAFELIST = Safelist.relaxed() + .addAttributes("img", "src", "alt", "referrerpolicy", "loading") + .addAttributes("a", "href", "target", "rel"); + + private ScraperTextUtils() { + } + + static Integer parseIntSafe(String text) { + if (text == null) { + return null; + } + String digitsOnly = text.trim().replace(",", ""); + if (!digitsOnly.matches("-?\\d+")) { + return null; + } + try { + return Integer.parseInt(digitsOnly); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * 본문 컨테이너에서 원본 게시물의 형태(문단, 굵게, 링크, 이미지 위치)를 보존한 채로 + * 안전하게 걸러낸 HTML과 이미지 URL 목록을 뽑아낸다. + * img/a의 상대경로는 절대경로로 바꾼 뒤 Safelist로 스크립트 등 위험 요소만 제거한다. + */ + static PostDetail extractDetail(Element body) { + if (body == null) { + return PostDetail.EMPTY; + } + + Element working = body.clone(); + + Set imageUrls = new LinkedHashSet<>(); + for (Element img : working.select("img")) { + String src = img.attr("abs:src"); + if (src.isBlank()) { + img.remove(); + continue; + } + img.attr("src", src); + img.attr("referrerpolicy", "no-referrer"); + img.attr("loading", "lazy"); + imageUrls.add(src); + } + for (Element link : working.select("a")) { + String href = link.attr("abs:href"); + if (href.isBlank()) { + continue; + } + link.attr("href", href); + link.attr("target", "_blank"); + link.attr("rel", "noopener noreferrer"); + } + + String contentHtml = Jsoup.clean(working.html(), working.baseUri(), CONTENT_SAFELIST).trim(); + + return new PostDetail(contentHtml, List.copyOf(imageUrls)); + } +} diff --git a/src/main/java/company/specialsource/scraper/ScrapingScheduler.java b/src/main/java/company/specialsource/scraper/ScrapingScheduler.java new file mode 100644 index 0000000..06f50ad --- /dev/null +++ b/src/main/java/company/specialsource/scraper/ScrapingScheduler.java @@ -0,0 +1,46 @@ +package company.specialsource.scraper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ScrapingScheduler { + + // 사이트에 부담을 주지 않기 위해 게시판별 요청 사이 최소 간격을 둔다. + private static final long DELAY_BETWEEN_BOARDS_MS = 1500; + + private final BoardScraperService boardScraperService; + private final ScrapedPostSyncService scrapedPostSyncService; + + @Scheduled( + initialDelayString = "${scraping.initial-delay-ms:10000}", + fixedDelayString = "${scraping.interval-ms:600000}" + ) + public void scrapeAllBoards() { + for (BoardSource source : BoardSource.values()) { + try { + List posts = boardScraperService.scrape(source); + int savedCount = scrapedPostSyncService.saveNewPosts(source, posts); + log.info("[스크래핑] {}/{} - 수집 {}건, 신규 저장 {}건", + source.getSiteName(), source.getBoardName(), posts.size(), savedCount); + } catch (Exception e) { + log.warn("[스크래핑 실패] {}/{} - {}", source.getSiteName(), source.getBoardName(), e.getMessage()); + } + sleepQuietly(); + } + } + + private void sleepQuietly() { + try { + Thread.sleep(DELAY_BETWEEN_BOARDS_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9640b51..a7e2b3b 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -21,4 +21,11 @@ jwt: # 256비트 이상이어야 하므로 영문+숫자 (최소 32글자 이상) secret: c3ByaW5nYm9vdC1qd3Qtc2VjcmV0LWtleS1zcGVjaWFsc291cmNlLWhvbWVwYWdlLWJhY2tlbmQtMjAyNg== # 토큰 유효 시간 (1시간으로 설정 = 60분 * 60초 * 1000밀리초) - expiration-time: 3600000 \ No newline at end of file + expiration-time: 3600000 + +scraping: + initial-delay-ms: 10000 # 앱 기동 후 첫 스크래핑까지 대기 시간 + interval-ms: 600000 # 게시판 전체 스크래핑 주기 (10분) + retention-days: 7 # 이 기간(scraped_at 기준)이 지난 게시물은 자동 삭제 + cleanup-initial-delay-ms: 60000 # 앱 기동 후 첫 정리 작업까지 대기 시간 + cleanup-interval-ms: 86400000 # 정리 작업 주기 (24시간) \ No newline at end of file diff --git a/src/test.http b/src/test.http new file mode 100644 index 0000000..aa14cf3 --- /dev/null +++ b/src/test.http @@ -0,0 +1,8 @@ +POST http://localhost:8080/api/login +#POST http://192.168.0.215:18080/api/login +Content-Type: application/json + +{ + "username": "admin", + "password": "1234" +} \ No newline at end of file diff --git a/src/test/java/company/specialsource/SpecialsourceHomepageApplicationTests.java b/src/test/java/company/specialsource/SpecialsourceHomepageApplicationTests.java new file mode 100644 index 0000000..05fff05 --- /dev/null +++ b/src/test/java/company/specialsource/SpecialsourceHomepageApplicationTests.java @@ -0,0 +1,13 @@ +package company.specialsource; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpecialsourceHomepageApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/company/specialsource/config/JasyptConfigTest.java b/src/test/java/company/specialsource/config/JasyptConfigTest.java new file mode 100644 index 0000000..f493e42 --- /dev/null +++ b/src/test/java/company/specialsource/config/JasyptConfigTest.java @@ -0,0 +1,37 @@ +package company.specialsource.config; + +import org.jasypt.encryption.StringEncryptor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class JasyptConfigTest { + + private final StringEncryptor stringEncryptor; + + JasyptConfigTest(@Qualifier("jasyptStringEncryptor") StringEncryptor stringEncryptor) { + this.stringEncryptor = stringEncryptor; + } + + @Test + void encryptDatasourcePassword() { + String plainPassword = "q1w2e3r4$"; + + String encryptedPassword = stringEncryptor.encrypt(plainPassword); + String decryptedPassword = stringEncryptor.decrypt(encryptedPassword); + + System.out.println("암호화 전:"); + System.out.println(plainPassword); + + System.out.println("암호화 결과:"); + System.out.println("ENC(" + encryptedPassword + ")"); + + System.out.println("복호화 확인:"); + System.out.println(decryptedPassword); + + assertThat(decryptedPassword).isEqualTo(plainPassword); + } +} diff --git a/src/test/java/company/specialsource/scraper/BoardParserTest.java b/src/test/java/company/specialsource/scraper/BoardParserTest.java new file mode 100644 index 0000000..0d72346 --- /dev/null +++ b/src/test/java/company/specialsource/scraper/BoardParserTest.java @@ -0,0 +1,82 @@ +package company.specialsource.scraper; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 실제 저장해둔 게시판 HTML 스냅샷으로 파서가 제목/링크를 제대로 뽑아내는지 검증한다. + * 네트워크 접속 없이 동작하므로 사이트 구조가 바뀌지 않는 한 항상 재현 가능하다. + */ +class BoardParserTest { + + private Document loadFixture(String fileName) throws IOException { + try (InputStream in = getClass().getResourceAsStream("/scraper/" + fileName)) { + assertThat(in).as("fixture %s must exist", fileName).isNotNull(); + return Jsoup.parse(in, "UTF-8", "https://bbs.ruliweb.com/"); + } + } + + @Test + void ruliwebBoardListParser_parsesBestBoard() throws IOException { + Document doc = loadFixture("ruliweb_best.html"); + + List posts = new RuliwebBoardListParser().parse(doc); + + assertThat(posts).isNotEmpty(); + ScrapedPostDto first = posts.get(0); + assertThat(first.title()).isNotBlank(); + assertThat(first.link()).startsWith("https://bbs.ruliweb.com/best/board/"); + assertThat(first.author()).isNotBlank(); + } + + @Test + void ruliwebBoardListParser_excludesNoticeRowsOnMarketBoard() throws IOException { + Document doc = loadFixture("ruliweb_market.html"); + + List posts = new RuliwebBoardListParser().parse(doc); + + assertThat(posts).isNotEmpty(); + assertThat(posts).noneMatch(p -> p.title().contains("불법촬영물등 유통 방지")); + } + + @Test + void ruliwebMypiParser_parsesBestWidget() throws IOException { + Document doc = loadFixture("ruliweb_mypi.html"); + + List posts = new RuliwebMypiParser().parse(doc); + + assertThat(posts).isNotEmpty(); + assertThat(posts.get(0).title()).isNotBlank(); + assertThat(posts.get(0).link()).startsWith("http"); + assertThat(posts).allMatch(p -> p.link().contains("ruliweb.com")); + } + + @Test + void ruliwebMypiParser_excludesAdLinksMixedIntoWidget() throws IOException { + Document doc = loadFixture("ruliweb_mypi.html"); + + List posts = new RuliwebMypiParser().parse(doc); + + assertThat(posts).noneMatch(p -> p.title().contains("인터넷 가입 변경 지원금")); + assertThat(posts).noneMatch(p -> p.link().contains("dajooda.com")); + } + + @Test + void dcGalleryParser_excludesNoticeAndSurveyRows() throws IOException { + Document doc = loadFixture("dc_bjj.html"); + + List posts = new DcGalleryParser().parse(doc); + + assertThat(posts).isNotEmpty(); + assertThat(posts).noneMatch(p -> p.title().contains("주짓수 갤러리 가이드")); + assertThat(posts).noneMatch(p -> p.title().contains("말빨로 정치인도")); + assertThat(posts.get(0).author()).isNotBlank(); + } +} diff --git a/src/test/java/company/specialsource/scraper/DetailParserTest.java b/src/test/java/company/specialsource/scraper/DetailParserTest.java new file mode 100644 index 0000000..76f4db6 --- /dev/null +++ b/src/test/java/company/specialsource/scraper/DetailParserTest.java @@ -0,0 +1,44 @@ +package company.specialsource.scraper; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.assertj.core.api.Assertions.assertThat; + +class DetailParserTest { + + private Document loadFixture(String fileName) throws IOException { + try (InputStream in = getClass().getResourceAsStream("/scraper/" + fileName)) { + assertThat(in).as("fixture %s must exist", fileName).isNotNull(); + return Jsoup.parse(in, "UTF-8", "https://bbs.ruliweb.com/"); + } + } + + @Test + void ruliwebPostDetailParser_extractsTextAndImages() throws IOException { + Document doc = loadFixture("ruliweb_post_market.html"); + + PostDetail detail = new RuliwebPostDetailParser().parse(doc); + + assertThat(Jsoup.parse(detail.content()).text()).contains("핫딜관리자입니다"); + assertThat(detail.content()).contains(" + + + + 주짓수 마이너 갤러리 - 커뮤니티 포털 디시인사이드 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +

+ + + 디시인사이드 + + + 갤러리 + +

+ + + + + + + + +
+
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + +
+ + + + + + + +
+ + + + + + + + +
+ +
+ + + + +
+ +
+ +
+ + +
+
+ + +
+ +
+ + + + +
+ + + + + +
+ + + + + + +
+
+ +
+

마이너 갤러리 이슈박스, 최근방문 갤러리

+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ + +

갤러리 리스트 영역

+
+
+ + + + + + + + +
+ +
+
+
+ + + +
+
+  글쓰기 +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
갤러리 리스트
번호말머리제목글쓴이작성일조회추천
-설문말빨로 정치인도 이길 것 같은 스타는?운영자26/08/10--
217350공지 + + 주짓수 갤러리 가이드 + + + + +
+ 데쿠빅데이터갤로그로 이동합니다.
+
26.04.2716091
220901일반 + + 다들 도복 가방 어떤 것을 쓰시나요? + + + [14] +
+ 생활체육주짓수갤로그로 이동합니다.
+
14:52640
220899일반 + + 도복에서 앵클락 거는데 상대가 내 목깃잡고 버티는거 카운터 못치나 + + + [4] +
+ ㅇㅇ갤로그로 이동합니다.
+
14:34590
220898일반 + + 리스펙트 주짓수? + + + [4] +
+ 동탄앤드류태켓갤로그로 이동합니다.
+
14:31780
220897일반 + + 입관비 받아도 되는 경우 + + + [1] +
+ 줏갤러(210.105)
+
14:30641
220896일반 + + 퇴관비도 도입하자 + + + +
+ 줏갤러(210.105)
+
14:28340
220895일반 + + 입관비라는게 좀 이상하긴 함ㅋㅋㅋ + + + +
+ 파워레슬러갤로그로 이동합니다.
+
14:19510
220894일반 + + 환불하는데 뭐이렇지 다들이럼? + + + [5] +
+ 줏갤러(112.145)
+
14:131031
220893일반 + + 입관비가 뭐예요 + + + [1] +
+ 줏갤러(118.235)
+
13:11920
220892일반 + + 흰띠 승급 + + + [2] +
+ 줏갤러(118.235)
+
13:011041
220891시청도 + + 여관원한테 바나나스플릿 쓴 후기 + + + [4] +
+ 줏갤러(210.105)
+
12:192396
220890일반 + + 입관비는 사실상 3개월등록 보너스용이지 + + + [3] +
+ ㅇㅇ(118.235)
+
12:051560
220889일반 + + 입관비란게 있는지도 몰랐음 + + + [1] +
+ 줏갤러(58.237)
+
11:48990
220888일반 + + 안다리걸기 ouchi gari 질문 + + + [4] +
+ ㅇㅇ(223.38)
+
11:35830
220887일반 + + 입관비 요구하는 관장이 병신같으면 개추 + + + +
+ ㅇㅇ(118.235)
+
11:22754
220886일반 + + 주짓수 4점으로 기어서 태클(앵클픽) 가는거 반칙임? + + + [7] +
+ ㅇㅇ(183.100)
+
11:191370
220885일반 + + 몸무게 많이 차이나는 상대 가드 추천좀 + + + [7] +
+ 줏갤러(180.81)
+
11:091330
220884일반 + + 이재영선수 어떻게 되는거요? + + + [6] +
+ 줏갤러(220.79)
+
09:204301
220883일반 + + 중량급 여성은 어떻게 상대하지 + + + [2] +
+ 토토또또갤로그로 이동합니다.
+
08:591430
220882일반 + + 선수자격 어찌되냐? + + + [12] +
+ 줏갤러(175.197)
+
06:344141
220881일반 + + 요즘도 이런 관장많냐? + + + [3] +
+ 줏갤러(1.251)
+
04:272791
220880일반 + + 스파이더맨 겁나 재미없네 + + + +
+ 줏갤러(1.252)
+
03:181710
220879일반 + + 주갤러들아 디시콘 만들었는데 념글 가능? + + + [3] +
+ 미식축구파이터갤로그로 이동합니다.
+
02:281506
220878일반 + + 몸 내구성 높이는 법 + + + [1] +
+ 미식축구파이터갤로그로 이동합니다.
+
02:221820
220877일반 + + 하 니찌반 존나 비싸네 + + + [4] +
+ ㅇㅇ(211.234)
+
01:451710
220876일반 + + 본인 여자랑할때 게임플랜 + + + [6] +
+ 줏갤러(121.132)
+
00:4743119
220875일반 + + 간장충들은 왜 입관비 말만 나오면 벌벌떨면서 개발작함? + + + +
+ ㅇㅇ(59.13)
+
00:461302
220873일반 + + 이번에 오디세이 아가멤논 아우라만 보면.. + + + [3] +
+ 줏갤러(27.35)
+
00:152261
220872일반 + + 드디어 여자랑 스파링 한다고 신나서 했는데 + + + [3] +
+ ㅇㅇ(211.36)
+
08.1039510
220871일반 + + 본인 여잔데 + + + [17] +
+ 줏갤러(115.136)
+
08.104511
220869일반 + + 입관비 3만원 존나 아까워서 걍 거르고싶은데 + + + [13] +
+ ㅇㅇ(211.50)
+
08.103360
220868일반 + + 정강이 + + + [3] +
+ 줏갤러(39.121)
+
08.10890
220867일반 + + 델라히바가드 상태에서 상대방 반대쪽 다리 발로 차는거 + + + [6] +
+ ㅇㅇ갤로그로 이동합니다.
+
08.101810
220866일반 + + 운동 빡세게 하면 + + + [1] +
+ 줏갤러(125.138)
+
08.101110
220865일반 + + 이긴적이 없는데 대회 나가는거 도움될까 + + + [13] +
+ 오잉크갤로그로 이동합니다.
+
08.102520
220864일반 + + 태클중에서 한쪽다리로 상대 발 꼬면서 넘어지는거 그게 이름이 뭐임? + + + [5] +
+ ㅇㅇ갤로그로 이동합니다.
+
08.101650
220863일반 + + 여자라고 살살하는게 어딨어! + + + [3] +
+ 틀짓수갤로그로 이동합니다.
+
08.102361
220862일반 + + 레그핀 질문, 발기술질문 + + + [6] +
+ 하리보좋아합니다(110.46)
+
08.101260
220861일반 + + 체육관 많이나가면 관장님이 안좋아하시나 + + + [6] +
+ ㅇㅇ(118.235)
+
08.103590
220860일반 + + 체육관들 가라 + + + [1] +
+ 파워레슬러갤로그로 이동합니다.
+
08.101560
220859일반 + + 이사미서울 반값 세일 중 + + + +
+ 줏갤러(118.32)
+
08.101541
220858일반 + + 도장에서 여자 관원 상대하는 법 알려준다 + + + [3] +
+ 닝닝(118.32)
+
08.103192
220857일반 + + 떼라한테 다해봤는데 + + + [5] +
+ 틀짓수갤로그로 이동합니다.
+
08.103290
220856일반 + + 수원 근처 8월9월 대회 추천 + + + [2] +
+ 줏갤러(119.194)
+
08.101610
220854일반 + + 여자한테 바나나스플릿 하면 안되지? + + + [14] +
+ 줏갤러(221.157)
+
08.104230
220853일반 + + 바나나 스플릿에 맛들림 + + + [1] +
+ 파워레슬러갤로그로 이동합니다.
+
08.101880
220852일반 + + 화이트 70 마스터 출전하는데 평체 몇정도랑 하는거냐 + + + [13] +
+ 줏갤러(180.81)
+
08.102340
220851일반 + + 여기 다 모쏠에겐남들밖에없나? + + + [3] +
+ ㅇㅇ(211.234)
+
08.102230
220850일반 + + 어우 손가락 아파 + + + [3] +
+ 파워레슬러갤로그로 이동합니다.
+
08.101170
220849일반 + + 여자랑 할때는 힘을 어느정도 써야함? + + + [30] +
+ 줏갤러(211.33)
+
08.103730
+ +
+
+
+
+ + +
+ + + + + + +
+ +
+ + +
+
+ + + + + +
+
+
+
+ +
+ +
+ +
+ + + + +
+ + + +
+
+ 갤러리 내부 검색 +
+
+ +
+ 제목+내용게시물 정렬 옵션 + +
+ +
+ +
+
+
+
+
+ + +
+

오른쪽 컨텐츠 영역

+ + +
+ + + +
+ + + +
+ + + +
+
+
+

실시간 베스트

+
+
+ 1/8 +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+

디시미디어

+

디시이슈

+
+
+ 1/2 +
+ + +
+
+ + + + +
+
+ + +
+ +
+ + +
+
+ + + +
+
+
+ + + +
+ + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/scraper/dc_post.html b/src/test/resources/scraper/dc_post.html new file mode 100644 index 0000000..e7f649c --- /dev/null +++ b/src/test/resources/scraper/dc_post.html @@ -0,0 +1,5023 @@ + + + + + 다들 도복 가방 어떤 것을 쓰시나요? - 주짓수 마이너 갤러리 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +

+ + + 디시인사이드 + + + 갤러리 + +

+ + + + + + + + +
+
+ +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + +
+ + + + + + + +
+ + + + + + + + +
+ +
+ + + + +
+ + + + +
+
+ + +
+
+ + +
+ +
+ + + + +
+ + + + + + +
+ + + + + + +
+
+ +
+

마이너 갤러리 이슈박스, 최근방문 갤러리

+
+ + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

갤러리 본문 영역

+
+
+
+ +

+ [일반] 다들 도복 가방 어떤 것을 쓰시나요?

+ +
+
+ + 생활체육주짓수갤로그로 이동합니다. 2026.08.11 14:52:59 + +
+
+ + 조회 126 + 추천 0 + 댓글 20 +
+
+
+ +
+
+
+
+
+

저는 출퇴근 소요 시간이 편도 1시간이라 퇴근 후에 바로 도장에 갑니다.

그래서 항상 가방에 도복과 벨트, 물통과 테이프와 마우스피스 따위를 넣어 놓은 파우치를 챙깁니다.


백팩이 확실히 이것저것 챙겨 다니기는 편하지만 출퇴근 좁은 지하철에서 다소 불편하고

에스테틱 도복과 같이 딸려온 에코백은 크기는 괜찮은데 딱 잡아주지 못하는 느낌... -_-



24b0d121e09c28a8699fe8b115ef046c68f0284799


이런 헬멧백 같은 것도 나쁘지 않아 보이는데 목적에 맞을지 모르겠네요.

저처럼 가방 매일 들고 다니시는 분들은 어떻게 들고 다니시나요??


 

+
+ + + + + + + +
+ + +
+
+ + + + + + + + + +
+ + +
+

추천 비추천

+
+
+
+

0

+

+ + 고정닉 + + 0 +

+
+ +
+
+ +
+

0

+
+
+
+
+ + + + + + + +
+
+ +
+ + + + +
+
+ +
+ 원본 첨부파일 1 +
+ +
+
+ +
+
+

댓글 영역

+
+
+
+ 전체 댓글 0개 + +
+ + + + + + + + + + + + + + + + +
+ + + + + + +
+
+ 본문 보기 + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+
+ + +
+
+
+ + +
+
+
+ +
+
+ + + +
+
+
+
+ + + + + + + + + + + +
+ + + + + + +
+
+ + + +
+
+
+
+ + + +
+ +
+ + +
+

하단 갤러리 리스트 영역

+ +
+ +
+

왼쪽 컨텐츠 영역

+
+

갤러리 리스트 영역

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
갤러리 리스트
번호말머리제목글쓴이작성일조회추천
-설문말빨로 정치인도 이길 것 같은 스타는?운영자26/08/10--
217350공지 + + 주짓수 갤러리 가이드 + + + + +
+ 데쿠빅데이터갤로그로 이동합니다.
+
26.04.2716091
220906일반 + + 울 도장 몇년동안 + + + +
+ 줏갤러(211.226)
+
16:2920
220905일반 + + 밤꽃냄새 티남? + + + +
+ ㅇㅇ(211.234)
+
16:15190
220904일반 + + 사쿠라바 카즈시 좋아하는 이유 + + + [1] +
+ 틀짓수갤로그로 이동합니다.
+
16:14300
220903일반 + + 이거 진짜 + + + +
+ 틀짓수갤로그로 이동합니다.
+
16:06340
220902일반 + + 요새도 띠로 갑질하는 간장님들계신가? + + + [2] +
+ 줏갤러(1.249)
+
15:43840
일반 + + 다들 도복 가방 어떤 것을 쓰시나요? + + + [20] +
+ 생활체육주짓수갤로그로 이동합니다.
+
14:521260
220899일반 + + 도복에서 앵클락 거는데 상대가 내 목깃잡고 버티는거 카운터 못치나 + + + [8] +
+ ㅇㅇ갤로그로 이동합니다.
+
14:341170
220898일반 + + 리스펙트 주짓수? + + + [7] +
+ 동탄앤드류태켓갤로그로 이동합니다.
+
14:311400
220897일반 + + 입관비 받아도 되는 경우 + + + [4] +
+ 줏갤러(210.105)
+
14:301021
220896일반 + + 퇴관비도 도입하자 + + + +
+ 줏갤러(210.105)
+
14:28571
220895일반 + + 입관비라는게 좀 이상하긴 함ㅋㅋㅋ + + + +
+ 파워레슬러갤로그로 이동합니다.
+
14:19710
220894일반 + + 환불하는데 뭐이렇지 다들이럼? + + + [6] +
+ 줏갤러(112.145)
+
14:131441
220893일반 + + 입관비가 뭐예요 + + + [1] +
+ 줏갤러(118.235)
+
13:111080
220892일반 + + 흰띠 승급 + + + [2] +
+ 줏갤러(118.235)
+
13:011221
220891시청도 + + 여관원한테 바나나스플릿 쓴 후기 + + + [4] +
+ 줏갤러(210.105)
+
12:192727
220890일반 + + 입관비는 사실상 3개월등록 보너스용이지 + + + [3] +
+ ㅇㅇ(118.235)
+
12:051690
220889일반 + + 입관비란게 있는지도 몰랐음 + + + [1] +
+ 줏갤러(58.237)
+
11:481080
220888일반 + + 안다리걸기 ouchi gari 질문 + + + [4] +
+ ㅇㅇ(223.38)
+
11:35940
220887일반 + + 입관비 요구하는 관장이 병신같으면 개추 + + + +
+ ㅇㅇ(118.235)
+
11:22907
220886일반 + + 주짓수 4점으로 기어서 태클(앵클픽) 가는거 반칙임? + + + [8] +
+ ㅇㅇ(183.100)
+
11:191540
220885일반 + + 몸무게 많이 차이나는 상대 가드 추천좀 + + + [8] +
+ 줏갤러(180.81)
+
11:091500
220884일반 + + 이재영선수 어떻게 되는거요? + + + [6] +
+ 줏갤러(220.79)
+
09:204551
220883일반 + + 중량급 여성은 어떻게 상대하지 + + + [2] +
+ 토토또또갤로그로 이동합니다.
+
08:591540
220882일반 + + 선수자격 어찌되냐? + + + [12] +
+ 줏갤러(175.197)
+
06:344331
220881일반 + + 요즘도 이런 관장많냐? + + + [3] +
+ 줏갤러(1.251)
+
04:272921
220880일반 + + 스파이더맨 겁나 재미없네 + + + +
+ 줏갤러(1.252)
+
03:181790
220879일반 + + 주갤러들아 디시콘 만들었는데 념글 가능? + + + [3] +
+ 미식축구파이터갤로그로 이동합니다.
+
02:281546
220878일반 + + 몸 내구성 높이는 법 + + + [1] +
+ 미식축구파이터갤로그로 이동합니다.
+
02:221910
220877일반 + + 하 니찌반 존나 비싸네 + + + [4] +
+ ㅇㅇ(211.234)
+
01:451780
220876일반 + + 본인 여자랑할때 게임플랜 + + + [6] +
+ 줏갤러(121.132)
+
00:4744519
220875일반 + + 간장충들은 왜 입관비 말만 나오면 벌벌떨면서 개발작함? + + + +
+ ㅇㅇ(59.13)
+
00:461332
220873일반 + + 이번에 오디세이 아가멤논 아우라만 보면.. + + + [3] +
+ 줏갤러(27.35)
+
00:152281
220872일반 + + 드디어 여자랑 스파링 한다고 신나서 했는데 + + + [3] +
+ ㅇㅇ(211.36)
+
08.1041210
220871일반 + + 본인 여잔데 + + + [17] +
+ 줏갤러(115.136)
+
08.104641
220869일반 + + 입관비 3만원 존나 아까워서 걍 거르고싶은데 + + + [13] +
+ ㅇㅇ(211.50)
+
08.103380
220868일반 + + 정강이 + + + [3] +
+ 줏갤러(39.121)
+
08.10900
220867일반 + + 델라히바가드 상태에서 상대방 반대쪽 다리 발로 차는거 + + + [6] +
+ ㅇㅇ갤로그로 이동합니다.
+
08.101840
220866일반 + + 운동 빡세게 하면 + + + [1] +
+ 줏갤러(125.138)
+
08.101120
220865일반 + + 이긴적이 없는데 대회 나가는거 도움될까 + + + [13] +
+ 오잉크갤로그로 이동합니다.
+
08.102570
220864일반 + + 태클중에서 한쪽다리로 상대 발 꼬면서 넘어지는거 그게 이름이 뭐임? + + + [5] +
+ ㅇㅇ갤로그로 이동합니다.
+
08.101700
220863일반 + + 여자라고 살살하는게 어딨어! + + + [3] +
+ 틀짓수갤로그로 이동합니다.
+
08.102431
220862일반 + + 레그핀 질문, 발기술질문 + + + [6] +
+ 하리보좋아합니다(110.46)
+
08.101290
220861일반 + + 체육관 많이나가면 관장님이 안좋아하시나 + + + [6] +
+ ㅇㅇ(118.235)
+
08.103630
220860일반 + + 체육관들 가라 + + + [1] +
+ 파워레슬러갤로그로 이동합니다.
+
08.101600
220859일반 + + 이사미서울 반값 세일 중 + + + +
+ 줏갤러(118.32)
+
08.101571
220858일반 + + 도장에서 여자 관원 상대하는 법 알려준다 + + + [3] +
+ 닝닝(118.32)
+
08.103222
220857일반 + + 떼라한테 다해봤는데 + + + [5] +
+ 틀짓수갤로그로 이동합니다.
+
08.103300
220856일반 + + 수원 근처 8월9월 대회 추천 + + + [2] +
+ 줏갤러(119.194)
+
08.101620
220854일반 + + 여자한테 바나나스플릿 하면 안되지? + + + [14] +
+ 줏갤러(221.157)
+
08.104260
220853일반 + + 바나나 스플릿에 맛들림 + + + [1] +
+ 파워레슬러갤로그로 이동합니다.
+
08.101890
+ +
+ +
+
+ + + + + +
+
+
+
+ +
+ +
+ +
+ + + +
+ + +
+
+ 갤러리 내부 검색 +
+
+ +
+ 제목+내용게시물 정렬 옵션 + +
+ +
+ +
+
+
+ +
+
+ + +
+

오른쪽 컨텐츠 영역

+ + +
+ + + +
+
+ + + +
+
+
+

실시간 베스트

+
+
+ 1/8 +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+

디시미디어

+

디시이슈

+
+
+ 1/2 +
+ + +
+
+ + + + +
+
+ + + + + +
+ +
+
+
+ +
+ +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+ + + +
+ + + +
+ + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/scraper/ruliweb_best.html b/src/test/resources/scraper/ruliweb_best.html new file mode 100644 index 0000000..0a7755b --- /dev/null +++ b/src/test/resources/scraper/ruliweb_best.html @@ -0,0 +1,3025 @@ + + + + 루리웹 RULIWEB + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ +
+
+ + + + + + +
+
+
+
+
+

본문

+
+
+
+
+
+
+
+
+ + + +
+
+ + +
+ +
+ +
+ 100추 +
스킨
+ + + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 76277095 + + + 1 + 가슴 따뜻해지는 갸루 구원 순애 만화 + (100) + + + 루리웹-2384037995 + 129 + 19249 + + 11:56
+ 76277509 + + + 2 + 요즘 인스타 가면 싸우는 현상 + (103) + + + candy pop + 157 + 17911 + + 12:37
+ 76276515 + + + 3 + 더쿠 회원이 정리한 4대째 의사집안 경력 + (97) + + + Birdmissile + 244 + 29829 + + 10:57
+ 106278 + + + 핫딜 + + [네이버페이] 일일적립, 클릭 29원, 라이브예고 14원 + (26) + + + 반코이남다 + 189 + 33233 + + 00:16
+ 1899851 + + + + + 요즘 인스타 가면 싸우는 현상 + (104) + + + candy pop + 157 + 18030 + + 12:37
+ 1899850 + + + + + 퇴사할때 자기가 만든 엑셀 매크로 삭제한 사람.jpg + (117) + + + YuJimin + 227 + 26603 + + 12:16
+ 1899849 + + + + + 하영 말 뒤집었네 + (76) + + + 크로스핏가기싫어 + 182 + 33637 + + 11:30
+ 1899848 + + + + + 토비 스파이더맨이 유독 가난했던 이유 + (65) + + + 불꽃저그 + 158 + 19398 + + 11:28
+ 1899847 + + + + + 몽골이 한짓에 비해서 욕을 안 먹는 이유 + (65) + + + 루리웹-1902869157 + 139 + 21916 + + 10:50
+ 1899846 + + + + + 슈로대)코드기어스가 일단 로봇은 외모가 중요하다는 것을 보여주는 것 아닐까? + (75) + + + 큐베의기묘한모험 + 61 + 12521 + + 11:00
+ 1899845 + + + + + 더쿠 회원이 정리한 4대째 의사집안 경력 + (98) + + + Birdmissile + 245 + 30422 + + 10:57
+ 1899844 + + + + + 그 배우 아버지가 쓴 재밌는 글 찾았음 + (71) + + + Birdmissile + 226 + 24955 + + 10:30
+ 1899843 + + + + + 여행비 엔빵 논란 + (115) + + + 대지뇨속 + 63 + 25672 + + 10:38
+ 1899842 + + + + + 여초사무실 컴터 수리일지 + (86) + + + 두류미 + 165 + 28886 + + 10:06
+ 1899841 + + + + + 이영도가 말하는 연좌제 논란.jpg + (110) + + + 루리웹-3470091471 + 259 + 31002 + + 10:19
+ 1899840 + + + + + 충격 서울 아파트 보유세 뉴욕보다 높아.... + (175) + + + 루리웹-7222436521 + 147 + 25880 + + 10:04
+ 1899839 + + + + + 귀지 빼고 8천원 청구한 소아과 + (80) + + + 아자르르르 + 151 + 21699 + + 09:42
+ 1899838 + + + + + 히로아카 최초엔딩 나왔을때는 체인소맨이랑 비교도 안됐음ㅋㅋㅋㅋ + (76) + + + 롱파르페 + 84 + 18141 + + 09:50
+ 1899837 + + + + + 하영 측, 증조부 친일 의혹 사실무근 입장 사과…“마음 매우 무거워” + (112) + + + 받은추천수7개 + 130 + 29795 + + 09:22
+ 1899836 + + + + + 귀지 빼고 8천원 청구한 소아과 + (110) + + + 루리웹-1098847581 + 281 + 42395 + + 08:46
+ 1899835 + + + + + 어느 작가가 휴재를 한 이유 + (78) + + + 루리웹-9135322191 + 146 + 36908 + + 08:25
+ 1899834 + + + + + “안전벨트 안 맬거야!”… 어린아이 고집에 비행기 결국 못 떴다 + (86) + + + 밀떡볶이 + 132 + 48642 + + 06:26
+ 1899833 + + + + + 조상님 자랑은 아니고 아부지 자랑 + (42) + + + 루근l웹-1234567890 + 107 + 28117 + + 00:42
+ 1899832 + + + + + 벨튀 신고했더니 집주인에게 벌금이 나옴 + (66) + + + error37 + 89 + 40286 + + 01:31
+ 1899831 + + + + + 재패니즈 우익의 인지부조화 + (65) + + + 루리웹-719126279 + 237 + 37982 + + 03:33
+ 1899830 + + + + + 본인은 진짜 한국인으로서의 정체성이고 친일같은 건 아무것도 몰랐다면 + (53) + + + 보스맨 + 77 + 30429 + + 02:08
+ 1899829 + + + + + 초극단적 다이어트로 학술지에 실린 사람의 손 + (68) + + + 빅맥 + 67 + 37547 + + 26.08.10
+ 1899828 + + + + + 프로그래머랑 수학자가 서로 다르게 받아드리는 수식 + (71) + + + 샤크드레이크 + 81 + 27882 + + 00:07
+ 1899827 + + + + + 눈 감고 애국가 1절 다 치면 폰 중독이래 + (70) + + + 갓지기 + 48 + 23599 + + 01:12
+ 1899826 + + + + + 친구가 코스어에 미쳐서 일상생활이 불가능하길래 극약처방해줌 + (95) + + + 루리웹-3458142289 + 189 + 36559 + + 26.08.10
+ 1899825 + + + + + 영화) 테넷 주인공 : 아직도 영화 이해 안돼 + (84) + + + 루리웹-2825740854 + 137 + 27440 + + 26.08.10
+ 1899824 + + + + + '거꾸로 태극기'에 분노했던 최시원 근황 + (105) + + + 루리웹-7222436521 + 244 + 45002 + + 26.08.10
+
+
+
+
+ +
+ BEST 뉴스 +
+
+ + + +
+
+
+
+
+ PC/온라인
+
+
+
+ 비디오/콘솔
+
+
+
+ 모바일
+
+
+
+ + + +
+
+
+
+
+
+ PC/온라인
+
+
+
+ 비디오/콘솔
+
+
+
+ 모바일
+
+
+
+ + + +
+
+
+
+
+
+ PC/온라인
+
+
+
+ 비디오/콘솔
+
+
+
+ 모바일
+
+
+
+ + + +
+
+
+ + + + +
+ +
+ BEST 유저정보 +
+
+
+
콘솔
+
+
+
PC
+
+
+
모바일
+
+
+
취미
+
+
+
+ + + + +
+
+ + +
+
+
+
+
+
+
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + > +
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+ +
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + + +
+
+ +
+
+ +
+
+
+
X
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/scraper/ruliweb_market.html b/src/test/resources/scraper/ruliweb_market.html new file mode 100644 index 0000000..b5d9c72 --- /dev/null +++ b/src/test/resources/scraper/ruliweb_market.html @@ -0,0 +1,3164 @@ + + + + 핫딜예판 유저 핫딜 중고장터 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ +
+
+ + + + + + +
+
+
+
+
+

본문

+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+ + 11번가   + 옥션   + 지마켓   + 신세계몰   + h몰   + gs이숍   + 롯데홈쇼핑   + 300x100.png +
+ 이마트 + 홈플러스 + 하이마트 + yes24 + ms + xbox + 애플닷컴 + 쿠팡 + 롯데온 + 알리익스프레스 + + + +
+
+ +
+
+
+
+
+
+
+ + +
+ + + + + +
+ +
+ +
+ +
+ 글쓰기 + + + + + +
공지
+
+ 스킨 +
+
+ 차단 +
+ + + + +
+
+
+
+ + +
+
+   + 인증글 + 베스트 + +
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ID + 구분 + 제목 + 글쓴이추천 조회 날짜
168 + 전체공지 + + + 불법촬영물등 유통 방지를 위한 이미지 필터링 시스템 적용 + + 8[RULIWEB] + + + + 2026.08.03
+ 106238 + + 상품권 + + + + [알리익스프레스] 알리's Pick (8월10일(월)~8월13일(목)) + (21) + + + _핫딜관리자 + + 19 + 55468 + 2026.08.09
+ 38542 + + 공지 + + + + 루리웹 핫딜/예판 유저게시판 통합 공지 + + + + _핫딜관리자 + + 254 + 4994364 + 2020.03.28
+ + + + BEST + + + + [스팀] 스팀 움직이는 아바타 1종와 프로필 액자 & 스티커 4종 (무료... + (3) + + + + + 49 + 14252 + 10:24
+ + + + BEST + + + + [네이버페이] 라방 5원 + (4) + + + + + 32 + 10635 + 09:43
+ + + + BEST + + + + [스팀] 헤비레인 1070원 + (51) + + + + + 21 + 16075 + 09:38
+ + + + BEST + + + + [땡겨요] 자담치킨 7천원,7번가피자 1만원 할인+배스킨 포장 할인 (8... + (6) + + + + + 9 + 10344 + 08:18
+ 106293 + 음식 + + + + + + 루랄 + 1 + 881 + 15:12
+ 106292 + 게임S/W + + + + + + hiro00 + 6 + 4226 + 14:09
+ 106291 + 게임S/W + + + + + + 윤아 + + 2739 + 14:00
+ 106289 + 게임S/W + + + + + + 윤아 + 2 + 8061 + 12:02
+ 106288 + 게임S/W + + + + + + 윤아 + 2 + 4338 + 12:01
+ 106287 + 게임S/W + + + + + + 윤아 + 1 + 5133 + 12:00
+ 106286 + 게임H/W + + + + + + 미돌이형 + 5 + 6905 + 11:24
+ 106285 + PC/가전 + + + + + + hawkins + 2 + 6309 + 11:10
+ 106284 + 게임S/W + + + + + + nokcha + 49 + 14265 + 10:24
+ 106283 + 상품권 + + + + + + 후히 + 32 + 10668 + 09:43
+ 106282 + 게임S/W + + + + + + nero111 + 21 + 16123 + 09:38
+ 106281 + 음식 + + + + + + 코스코수 + 4 + 4823 + 09:17
+ 106280 + 음식 + + + + + + 아타호-_- + 9 + 10359 + 08:18
+ 106279 + 음식 + + + + + + 루랄 + 2 + 4694 + 07:56
+ 106278 + 상품권 + + + + + + 반코이남다 + 189 + 33288 + 00:16
+ 106277 + PC/가전 + + + + + + berserkfan + 6 + 19133 + 2026.08.10
+ 106276 + 게임S/W + + + + + + 루리웹-5477322653 + 29 + 40242 + 2026.08.10
+ 106275 + PC/가전 + + + + + + ideality + 7 + 21918 + 2026.08.10
+ 106274 + 음식 + + + + + + 콜산난입 + 20 + 20581 + 2026.08.10
+ 106273 + 음식 + + + + + + 아타호-_- + 12 + 20595 + 2026.08.10
+ 106272 + 음식 + + + + + + 아타호-_- + 21 + 28064 + 2026.08.10
+ 106271 + 게임S/W + + + + + + RΔZΞR + 5 + 13289 + 2026.08.10
+ 106270 + 게임S/W + + + + + + 零~ZERO~ + 15 + 43199 + 2026.08.10
+ 106269 + 게임H/W + + + + + + 라이덴-7 + 7 + 13727 + 2026.08.10
+ 106268 + 음식 + + + + + + 루랄 + 5 + 9534 + 2026.08.10
+ 106267 + PC/가전 + + + + + + 엔챠. + 6 + 16426 + 2026.08.10
+ 106266 + 취미용품 + + + + + + 윤아 + 4 + 9642 + 2026.08.10
+ 106265 + 음식 + + + + + + 루랄 + 6 + 10083 + 2026.08.10
+
+ +
+ +
+ 글쓰기 + 총 73743개의 글이 있습니다. + +
+ +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + +
+ +
+
+ + + + +
+
+
+

게시판 관리자

+ +
+
+
+
+ + +
+
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + + +
+
+ +
+
+ +
+
+
+
X
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/scraper/ruliweb_mypi.html b/src/test/resources/scraper/ruliweb_mypi.html new file mode 100644 index 0000000..be3ee6f --- /dev/null +++ b/src/test/resources/scraper/ruliweb_mypi.html @@ -0,0 +1,2153 @@ + + + + + + + + + + + MYPI | 루리웹 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +

본문

+ +
+ + +
+ + + +
+
+ +
+
+
+ +
+
+ + +
+
+
    +

     마이피 맞춤검색

    + +
      + + + +
    +
    + +
      공지 : CSS 수정으로 관리 모드 접근 못 하는 분들은 루리웹정보에 관리 모드 링크가 있으니 확인해주세요. + +
+ +
+
+
+
+ +

마이피 최신글

+
+ ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
하영?돌아온leejh
펌)GPT로 만든 동영상(?) 관리 앱 (모자이크)'Ai
유튜브에 매해 조금씩 반프레스토의 SFC판 학교에서 있었던 무서..칸유대위
알뜰폰 요금제 중에서....驕慢[교만]의 墮天使
오버워치 광고 근황리치왕
(블룸버그) 채권, 인플레이션 우려로 하락 및 유가 상승: 시황..차지맨 켄!
[NSFW] Okiichloeorbduf
에어컨 분해청소뉴타입GTK
강릉 피서왔습니다.라트라비아타!
(블룸버그) 인텔, AI 수요 급증에 150억 달러 규모 신주 ..차지맨 켄!
펌)미래는 모두를 위한 것 by 마크 저커버그'Ai
윤석열 감방에 에어컨 설치 찬성하는 만화새가날아든다
부모님 kt쓰는데 이제야 알게된 사실!!V_xian
여권이 빨리도 발급되는군요 6일만에요시마.
마누라 마운자로 사줌ㅎ 뀨읭
자꾸만 보게 되는 천소윤 치어리더초월자
ㅎㅂ) 유치땅 화보초월자
콜라 1.25L 12병 5,000원 개꿀人生無想
급여 인상..✨風雨來記
데모부터 맛있는 오락실의 그게임 신작 타쯔진 우주닌자
걷기 포인트 없어진대서*하얀모자*
제 책상루리웹-012345678910
증명사진 드럽게 비싸네.네임드of네임드
에어컨아 근 한달간 고생했다...바이오맨!
PC구입 (집마홀릭 공구)뉴타입GTK
민주당 망했다고 ㅂㅆ 짓하는 국힘 뿌락치 마제스팅어GT
드디어 쓸모가 있어진 그놈.jpgwizwiz
드디어 별점을 얻어낸 하츄핑 영화흰곰총각
계산대 토끼의 역할 .gifFluffy♥
철저하게 망가지는 민주당.......새가날아든다
[주식] 국내 상장사의 7.4%가 시가총액 기준에 미달한 것으로..킹브레드레이
햄버거 단품 먹는 게 죄인가요?예숫님
이번 콜럼비아 지진 당시 영상RAHARU
젠존제 의현 그렸습니다.-약후-광홍
컴퓨터 빡세일 특가 9,149,680원~!~!!~~!킹브레드레이
니케 잘 모르는데 누가 부계 준다고 해서 추천 좀 부탁드립니다공허의코나투스
게임 과금모델saturnus
2013년 남아프리카공화국 피네타운 트럭 사고 관련 재판 결과입..엔에언봉버피나
We loved the blue최첨단인공지능!츄푸덕남편놈세끼
친일파의 후손인걸 알게된 인디밴드 보컬.jpgwizwiz
담배때문에 심히 일 때려치겠다는 소리가 왜날뾁에올인
와 중학교 내신 수학 시험문제 수준이...puchikon
석양이 진다 .gifFluffy♥
자려고 누웠는데 갑자기~ 루리웹인수위
[아르콘 소울] 주사위 조작 + 덱빌딩 + 로그라이크 신작 게임..P.E.K.O
확인 안 하고 써드파티 판매자 물건을 사버렸네전뇌전기
미연누나 인스타조미연💚
하영 배우님 응원합니다.부선정거OUT
260811 미연누나 김포공항 출국조미연💚
Senua - 공식 공개 예고편 | Xbox 게임 쇼케이스 20..하프라이프/
SRPG 황혼의 원정군...뜨끔뜨뜨근
요즘 마이피 방문이 뜸했던 듯최첨단인공지능!츄푸덕남편놈세끼
제가 최근에 좀 충격적인 경험을 한 일이 있습니다.루리웹-[7777777]
(ㅇㅎ) 푸짐한 파스타.jpgStephanie
떼껄룩흰곰총각
순대국밥이 맛없기도 힘든데... 그냥 술이 빠진건가?Xris C
006(OL) - 후방 AI초신속달팽이
벗겨버리기(후방 AI 영상)초신속달팽이
약과 받았어용 >ㅅ< 雪影
[ㅇㅎ]히이라기 리온-08-토마토-

+ + +
+
+
+
+

+ +
+
+

+
+
+
+ +
+
+
+ +
+
+
+ + +
+ + + + +
+ +
+ BEST 게시글 +
+
+ + + + +
+
+
+
+
+ 게임 +
+
+
+
+ 애니/책 +
+
+
+
+ 갤러리 +
+
+
+
+ 커뮤니티 +
+
+
+
+ + + + +
+
+
+
+
+
+ 게임 +
+
+
+
+ 애니/책 +
+
+
+
+ 갤러리 +
+
+
+
+ 커뮤니티 +
+
+
+
+ + + + +
+
+
+
+
+
+ 게임 +
+
+
+
+ 애니/책 +
+
+
+
+ 갤러리 +
+
+
+
+ 커뮤니티 +
+
+
+
+ + + + +
+
+
+
+
+
+ 게임 +
+
+
+
+ 애니/책 +
+
+
+
+ 갤러리 +
+
+
+
+ 커뮤니티 +
+
+
+
+ + + + +
+
+
+ + + + +
+ +
+ + + + + + + +
+
+ +
+ +
+ + +
+ + +
+ + + + +
+
+ +
+
+ +
+
+
+
X
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/scraper/ruliweb_post_market.html b/src/test/resources/scraper/ruliweb_post_market.html new file mode 100644 index 0000000..60413f4 --- /dev/null +++ b/src/test/resources/scraper/ruliweb_post_market.html @@ -0,0 +1,5469 @@ + + + + 알리익스프레스 알리s Pick 8월10일월8월13일목 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+ +
+
+ + + + + + +
+
+
+
+
+

본문

+
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+ + +
+ + + +
+ +
+
+
+ 글쓰기 +   + 목록 + | + 이전글 + | + 다음글 +
+ +
+
+ +
+ +
+
+
+ + + + +
+
+

+ + [상품권] [알리익스프레스] 알리's Pick (8월10일(월)~8월13일(목)) + + [21] +

+
+
+
+
+
+
+ profile_image_default +
+ +
+
+
+
+ + + + + + + + + + + +
+ + + + + + + + + + +
+
+
+ + + + + +
+
+ + (5328521) +
+ 19 + | + 21 + | + 55926 + | 비추력 3451 +
+ 프로필 열기/닫기 +
+ + +
+ +
+
+
+
+
+ + + + + + + + +
+ + +
+
+ +
+ + +
+
+ 글쓰기 + +
+ +
+
+
+ + + + +
+
+
+ +
+ + | + +
+ +
+ 목록 + | + 이전글 + | + 다음글 +
+ + +
+ +
+
+ +
+
+
+
+ +
+
+ + +
+ + + + + + + +
+
+ 댓글 + | + 총 21 개 +
+
+ 1 +
+ +
+ + + + + + + + + +  댓글 + +
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +

(IP보기클릭)119.202.***.***

+
+
+
+ BEST
+ 할인이 끝나면 뭐가시작되죠? +또 다시 할인이 시작된다 +
+
+
+ 26.08.09 10:53 + + + +
+
+
+ + +

(IP보기클릭)118.47.***.***

+
+
+
+ BEST
+ 다른말로 하면 상시 정가란 이야기 +
+
+
+ 26.08.09 12:16 + + + +
+
+
+ + +

(IP보기클릭)125.240.***.***

+
+
+
+ BEST
+ 그만큼 올림 ㅋ +???: 다시는 알리를 무시하지 마라 +
+
+
+ 26.08.09 15:31 + + + +
+
+
+ + +

(IP보기클릭)211.219.***.***

+
+
+
+ BEST
+ 쌀 쿠폰적용 다시 해줘 ㅜ +
+
+
+ 26.08.09 11:09 + + + +
+
+
+ + +

(IP보기클릭)222.251.***.***

+
+
+
+ BEST
+
+ + comment_img + +
+
+ 알리 어플이 언제부턴가 메인 화면이 아니라 사진처럼 나오는데 + +혹시 메인화면으로 다시 나오게 하는법 아시는분 계실까요? + +메인화면에 할인쿠폰이랑 나오는데 어느 순간부터 안나오네요;; +
+
+
+ 26.08.09 17:22 + + + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +

+ (IP보기클릭)119.202.***.***

+
+
+
+ BEST +
+ 할인이 끝나면 뭐가시작되죠? +또 다시 할인이 시작된다 +
+ + + + +
+
+ + 26.08.09 10:53 + + + +
+
+
+ + + +

+ (IP보기클릭)125.184.***.***

+
+
+
+ 밥똥던 +
+ 많이 이용해달라는 알리의 뜻. (최소 중국직구에 있어 경쟁사인 쿠팡이 나락가서 그러는것도 있지만) + + | + 26.08.09 11:00 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)118.47.***.***

+
+
+
+ BEST +
+ 밥똥던 +
+ 다른말로 하면 상시 정가란 이야기 + + | + 26.08.09 12:16 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)115.22.***.***

+
+
+
+ 밥똥던 +
+ 카드사 할인 포함 안되면 사실상 정가 + + | + 26.08.09 17:52 + | + + | + + | + + +
+ + + + +
+
+ + +

+ (IP보기클릭)211.219.***.***

+
+
+
+ BEST +
+ 쌀 쿠폰적용 다시 해줘 ㅜ +
+ + + + +
+
+ + 26.08.09 11:09 + + + +
+
+
+ + +

+ (IP보기클릭)115.138.***.***

+
+
+
+ 환율 떨어져서 쇼핑할만 할듯 +
+ + + + +
+
+ + 26.08.09 11:12 + + + +
+
+
+ + + +

+ (IP보기클릭)125.240.***.***

+
+
+
+ BEST +
+ 꼭지 +
+ 그만큼 올림 ㅋ +???: 다시는 알리를 무시하지 마라 + + | + 26.08.09 15:31 + | + + | + + | + + +
+ + + + +
+
+ + +

+ (IP보기클릭)222.251.***.***

+
+
+
+ BEST +
+
+ + comment_img + +
+
+ 알리 어플이 언제부턴가 메인 화면이 아니라 사진처럼 나오는데 + +혹시 메인화면으로 다시 나오게 하는법 아시는분 계실까요? + +메인화면에 할인쿠폰이랑 나오는데 어느 순간부터 안나오네요;; +
+ + + + +
+
+ + 26.08.09 17:22 + + + +
+
+
+ + + +

+ (IP보기클릭)125.184.***.***

+
+
+
+ kumdodori +
+ 보통 버그이기 때문에 캐시, 데이터를 날리거내 앱 재설치하시면 됩니다. + + | + 26.08.09 17:35 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)222.251.***.***

+
+
+
+ nana111 +
+ 둘다 해봐도 안되니;; 걍 pc로 써야할까 싶네요;; + +나한테 할인해주기가 싫었던걸까 ㅜ + + | + 26.08.09 18:45 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)140.248.***.***

+
+
+
+ kumdodori +
+ 캐시,데이터 날리고 재로그인 + + | + 26.08.10 00:36 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)222.251.***.***

+
+
+
+ lllIlIlIIlIIlI +
+ 해봤는데 안되네요 ㅜ 답변 감사합니다 + + | + 26.08.10 07:14 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)211.193.***.***

+
+
+
+ kumdodori +
+ 메인화면 개편됐네요 개구려짐 + + | + 26.08.10 12:30 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)175.195.***.***

+
+
+
+ 꽃보다요가 +
+ 버그가 아니라 개편인건가요? 할인쿠폰 보는게 사라졌어요;; + +어디서 보는지도 모르겠네요;; + + | + 26.08.10 16:03 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)121.131.***.***

+
+
+
+ kumdodori +
+ 이번꺼 13일까지는 크리에이터 데이라고 +보통 할인 받는거랑 달라서 안나오는게 정상일꺼에요 +13일이후에 우리가 알던 할인이벤트 진행한다고함 +이번에 나눴다고하더라고요 + + | + 26.08.10 19:39 + | + + | + + | + + +
+ + + + +
+
+ + + +

+ (IP보기클릭)222.251.***.***

+
+
+
+ Qupa +
+ 아 답변 감사합니다. 뭔가 바껴서 제것만 이상해진줄 알았네요 ㅎㅎ + + | + 26.08.10 20:28 + | + + | + + | + + +
+ + + + +
+
+ + +

+ (IP보기클릭)140.248.***.***

+
+
+
+
+ + comment_img + +
+
+ 감사합니다 알뜰폰 유심 끼워 쓸 패드 잘사갑니다. +
+ + + + +
+
+ + 26.08.10 00:36 + + + +
+
+
+ + +

+ (IP보기클릭)183.109.***.***

+
+
+
+ 카드 할인은 맨날 소진이래 +
+ + + + +
+
+ + 26.08.10 03:07 + + + +
+
+
+ + +

+ (IP보기클릭)119.197.***.***

+
+
+
+ 유튭 크리에이터들도 할인홍보 엄청 하던데 +무슨 할인이 1시간도 안되서 끝남..ㅋㅋㅋ +그것도 새벽인데.. 카드할인 이렇게 할꺼면 그냥 홍보를 하질 말지 +
+ + + + +
+
+ + 26.08.10 10:00 + + + +
+
+
+ + +

+ (IP보기클릭)220.127.***.***

+
+
+
+ 카드할인 벌써끝난게 맞나했는데 진짜 끝난거구나 +
+ + + + +
+
+ + 26.08.10 10:57 + + + +
+
+
+ + +

+ (IP보기클릭)117.111.***.***

+
+
+
+ 카할이 되면 쿠폰이 안먹고 쿠폰이 되면 카할이 안먹고 ㅠㅠ +쿠폰이 단일상품기준으로 바뀐건가요? +
+ + + + +
+
+ + 26.08.11 08:38 + + + +
+
+
+
+
+
+
+ +
+ 1 +
+ +
+ + + + + + + + + +  댓글 +
+
+
+
+ + + +

+ +
+
+ +
+
+
+ 글쓰기 +
+ +
+ 목록 + | + 이전글 + | + 다음글 +
+
+
+
+
+
+
+ + + + +
+ + + + + +
+ +
+ +
+ +
+ 글쓰기 + + + + + +
공지
+
+ 스킨 +
+
+ 차단 +
+ + + + +
+
+
+
+ + +
+
+   + 인증글 + 베스트 + +
+
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ID + 구분 + 제목 + 글쓴이추천 조회 날짜
168 + 전체공지 + + + 불법촬영물등 유통 방지를 위한 이미지 필터링 시스템 적용 + + 8[RULIWEB] + + + + 2026.08.03
+ 106238 + + 상품권 + + + + [알리익스프레스] 알리's Pick (8월10일(월)~8월13일(목)) + (21) + + + _핫딜관리자 + + 19 + 55786 + 2026.08.09
+ 38542 + + 공지 + + + + 루리웹 핫딜/예판 유저게시판 통합 공지 + + + + _핫딜관리자 + + 254 + 4994563 + 2020.03.28
+ + + + BEST + + + + [엑박/pc] 발더스게이트3 41,230원+[kt컨텐츠페이]할인 + (10) + + + + + 10 + 6902 + 14:09
+ + + + BEST + + + + [스팀] 스팀 움직이는 아바타 1종와 프로필 액자 & 스티커 4종 (무료... + (3) + + + + + 51 + 15311 + 10:24
+ + + + BEST + + + + [네이버페이] 라방 5원 + (4) + + + + + 33 + 11603 + 09:43
+ + + + BEST + + + + [스팀] 헤비레인 1070원 + (52) + + + + + 21 + 17299 + 09:38
+ 106295 + 게임S/W + + + + + + 라이덴-7 + + 207 + 16:30
+ 106294 + 취미용품 + + + + + + 윤아 + 1 + 1109 + 16:10
+ 106293 + 음식 + + + + + + 루랄 + 1 + 2045 + 15:12
+ 106292 + 게임S/W + + + + + + hiro00 + 10 + 6973 + 14:09
+ 106291 + 게임S/W + + + + + + 윤아 + 2 + 3601 + 14:00
+ 106289 + 게임S/W + + + + + + 윤아 + 2 + 8942 + 12:02
+ 106288 + 게임S/W + + + + + + 윤아 + 2 + 4946 + 12:01
+ 106287 + 게임S/W + + + + + + 윤아 + 1 + 5647 + 12:00
+ 106286 + 게임H/W + + + + + + 미돌이형 + 5 + 7484 + 11:24
+ 106285 + PC/가전 + + + + + + hawkins + 2 + 6807 + 11:10
+ 106284 + 게임S/W + + + + + + nokcha + 51 + 15381 + 10:24
+ 106283 + 상품권 + + + + + + 후히 + 33 + 11614 + 09:43
+ 106282 + 게임S/W + + + + + + nero111 + 21 + 17385 + 09:38
+ 106281 + 음식 + + + + + + 코스코수 + 4 + 5176 + 09:17
+ 106280 + 음식 + + + + + + 아타호-_- + 9 + 11256 + 08:18
+ 106279 + 음식 + + + + + + 루랄 + 2 + 4952 + 07:56
+ 106278 + 상품권 + + + + + + 반코이남다 + 192 + 34206 + 00:16
+ 106277 + PC/가전 + + + + + + berserkfan + 6 + 19482 + 2026.08.10
+ 106276 + 게임S/W + + + + + + 루리웹-5477322653 + 30 + 41106 + 2026.08.10
+ 106275 + PC/가전 + + + + + + ideality + 7 + 22278 + 2026.08.10
+ 106274 + 음식 + + + + + + 콜산난입 + 20 + 21135 + 2026.08.10
+ 106273 + 음식 + + + + + + 아타호-_- + 12 + 21273 + 2026.08.10
+ 106272 + 음식 + + + + + + 아타호-_- + 21 + 28769 + 2026.08.10
+ 106271 + 게임S/W + + + + + + RΔZΞR + 5 + 13562 + 2026.08.10
+ 106270 + 게임S/W + + + + + + 零~ZERO~ + 15 + 43833 + 2026.08.10
+ 106269 + 게임H/W + + + + + + 라이덴-7 + 7 + 13971 + 2026.08.10
+ 106268 + 음식 + + + + + + 루랄 + 5 + 9792 + 2026.08.10
+ 106267 + PC/가전 + + + + + + 엔챠. + 6 + 16743 + 2026.08.10
+
+ +
+ +
+ 글쓰기 + 총 73743개의 글이 있습니다. + +
+ +
+ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + +
+ +
+
+ + + + +
+
+
+

게시판 관리자

+ +
+
+
+
+ + + +
+
+
+ +
+ +
+ +
+ +
+ + + +
+ +
+ + + + +
+
+ +
+
+ +
+
+
+
X
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file