---
title: "상표권 위반 명품 가품 게시글 종합 분석 대시보드 v9.1 (중복 제거)"
output:
flexdashboard::flex_dashboard:
orientation: columns
vertical_layout: scroll
theme: cosmo
source_code: embed
---
```{r setup, include=FALSE}
# 패키지 설치 및 로드
required_packages <- c("flexdashboard", "tidyverse", "plotly", "DT", "stringr", "scales", "jsonlite")
for (pkg in required_packages) {
if (!require(pkg, character.only = TRUE)) {
install.packages(pkg, repos = "http://cran.r-project.org")
library(pkg, character.only = TRUE)
}
}
# 데이터 로드 - 여러 파일 형식 지원
# 파일 존재 여부를 자동으로 확인하여 로드합니다
# ========================================================================
# 🎯 자동 선택: 사용 가능한 파일 중 최선의 옵션 선택
# ========================================================================
# 파일 우선순위에 따라 자동 로드
if (file.exists("bquxjob_unique.json.gz")) {
# 옵션 1: 중복 제거 + 압축 (권장) ⭐⭐⭐
data <- fromJSON(gzfile("bquxjob_unique.json.gz"), flatten = TRUE)
cat("✅ 중복 제거 압축 파일 로드 (13,304개 고유 게시글)
")
} else if (file.exists("bquxjob_unique.json")) {
# 옵션 2: 중복 제거 + 원본 JSON
data <- fromJSON("bquxjob_unique.json", flatten = TRUE)
cat("✅ 중복 제거 JSON 파일 로드 (13,304개 고유 게시글)
")
} else if (file.exists("bquxjob_75e0b464_19a4d90a830.json.gz")) {
# 옵션 3: 중복 포함 + 압축
data <- fromJSON(gzfile("bquxjob_75e0b464_19a4d90a830.json.gz"), flatten = TRUE)
cat("⚠️ 중복 포함 압축 파일 로드 (24,815개, 중복 11,511개 포함)
")
} else if (file.exists("bquxjob_75e0b464_19a4d90a830.json")) {
# 옵션 4: 중복 포함 + 원본 JSON
data <- fromJSON("bquxjob_75e0b464_19a4d90a830.json", flatten = TRUE)
cat("⚠️ 중복 포함 JSON 파일 로드 (24,815개, 중복 11,511개 포함)
")
} else if (file.exists("sample_500.json")) {
# 옵션 5: 샘플 데이터
data <- fromJSON("sample_500.json", flatten = TRUE)
cat("📝 샘플 데이터 로드 (500개)
")
} else {
stop("❌ 데이터 파일을 찾을 수 없습니다!
",
"다음 파일 중 하나를 .Rmd 파일과 같은 폴더에 넣어주세요:
",
" - bquxjob_unique.json.gz (권장)
",
" - bquxjob_unique.json
",
" - bquxjob_75e0b464_19a4d90a830.json.gz
",
" - bquxjob_75e0b464_19a4d90a830.json
",
" - sample_500.json")
}
# ========================================================================
# 💡 수동 선택이 필요한 경우 아래 주석을 해제하세요
# ========================================================================
# 중복 제거 버전 (권장)
# data <- fromJSON(gzfile("bquxjob_unique.json.gz"), flatten = TRUE)
# data <- fromJSON("bquxjob_unique.json", flatten = TRUE)
# 중복 포함 버전
# data <- fromJSON(gzfile("bquxjob_75e0b464_19a4d90a830.json.gz"), flatten = TRUE)
# data <- fromJSON("bquxjob_75e0b464_19a4d90a830.json", flatten = TRUE)
# 샘플 데이터
# data <- fromJSON("sample_500.json", flatten = TRUE)
cat("데이터 로드 완료:", nrow(data), "행\n")
# 데이터 전처리
# 날짜 처리 - UTC 문자열 제거
data$created_at <- as.character(data$created_at)
data$updated_at <- as.character(data$updated_at)
data$created_at <- sub(" UTC$", "", data$created_at)
data$updated_at <- sub(" UTC$", "", data$updated_at)
data$created_at <- as.POSIXct(data$created_at, format="%Y-%m-%d %H:%M:%OS")
data$updated_at <- as.POSIXct(data$updated_at, format="%Y-%m-%d %H:%M:%OS")
# price는 이미 숫자형이므로 그대로 사용
data$price_numeric <- as.numeric(data$price)
# 나눔 표기 확인 - price가 0이거나 title/content에 '나눔'이 있는 경우
data$is_naum <- (data$price == 0) |
grepl("나눔", data$title, ignore.case = TRUE) |
grepl("나눔", data$content, ignore.case = TRUE)
# 브랜드 매핑 (대폭 확장)
brand_mapping <- list(
"샤넬" = c("샤넬", "chanel", "채널", "사넬"),
"루이비통" = c("루이비통", "루이", "louis vuitton", "lv", "비통"),
"구찌" = c("구찌", "gucci", "구씨"),
"에르메스" = c("에르메스", "hermes", "에루메스", "에르"),
"프라다" = c("프라다", "prada"),
"디올" = c("디올", "dior"),
"발렌시아가" = c("발렌시아가", "balenciaga", "발렌"),
"셀린느" = c("셀린느", "셀린", "celine", "셀리느"),
"펜디" = c("펜디", "fendi"),
"버버리" = c("버버리", "burberry", "버버"),
"롤렉스" = c("롤렉스", "rolex", "롤"),
"까르띠에" = c("까르띠에", "카르티에", "cartier", "까르띠", "카르띠에"),
"몽블랑" = c("몽블랑", "montblanc", "몽블"),
"오메가" = c("오메가", "omega"),
"보테가베네타" = c("보테가", "bottega", "베네타", "보떼가", "보테가베네타", "bottega veneta"),
"생로랑" = c("생로랑", "saint laurent", "ysl"),
"발렌티노" = c("발렌티노", "valentino"),
"지방시" = c("지방시", "givenchy"),
"톰브라운" = c("톰브라운", "thom browne"),
"메종마르지엘라" = c("메종마르지엘라", "maison margiela", "마르지엘라", "메종 마르지엘라"),
"고야드" = c("고야드", "goyard"),
"돌체앤가바나" = c("돌체앤가바나", "dolce & gabbana", "dolce", "가바나"),
"로에베" = c("로에베", "loewe"),
"미우미우" = c("미우미우", "miu miu"),
"베르사체" = c("베르사체", "versace"),
"불가리" = c("불가리", "bvlgari", "bulgari"),
"부쉐론" = c("부쉐론", "boucheron"),
"알렉산더맥퀸" = c("알렉산더 맥퀸", "알렉산더맥퀸", "alexander mcqueen", "맥퀸"),
"아르마니" = c("알마니", "아르마니", "armani"),
"오프화이트" = c("오프화이트", "off-white", "off white"),
"우영미" = c("우영미", "wooyoungmi"),
"티파니앤코" = c("티파니 앤코", "티파니앤코", "tiffany & co", "tiffany", "티파니"),
"프레드" = c("프레드", "fred"),
"페라가모" = c("페라가모", "ferragamo"),
"페라리" = c("페라리", "ferrari"),
"오데마피게" = c("오데마피게", "audemars piguet", "오데마"),
"톰포드" = c("톰 포드", "톰포드", "tom ford"),
"파텍필립" = c("파텍 필립", "파텍필립", "patek philippe", "파텍"),
"브루넬로쿠치넬리" = c("브루넬로 쿠치넬리", "브루넬로쿠치넬리", "brunello cucinelli", "쿠치넬리"),
"로로피아나" = c("로로 피아나", "로로피아나", "loro piana"),
"피비필로" = c("피비 필로", "피비필로", "phoebe philo"),
"제냐" = c("제냐", "zegna"),
"더로우" = c("더 로우", "더로우", "the row"),
"리모와" = c("리모와", "rimowa"),
"토즈" = c("토즈", "tod's", "tods"),
"몽클레르" = c("몽클레르", "moncler"),
"맥스마라" = c("맥스마라", "max mara"),
"클로에" = c("클로에", "chloe")
)
# 브랜드 감지 함수
detect_brand <- function(title, content) {
text <- tolower(paste(title, content))
for (brand in names(brand_mapping)) {
keywords <- brand_mapping[[brand]]
for (keyword in keywords) {
if (str_detect(text, tolower(keyword))) {
return(brand)
}
}
}
return("기타")
}
# 브랜드 컬럼 생성
cat("브랜드 분석 중...\n")
data$brand <- mapply(detect_brand, data$title, data$content)
# 품목 매핑 (세부 분류 추가)
item_mapping <- list(
# 가방 세부 분류
"가방-숄더백" = c("숄더백", "shoulder bag", "크로스백", "크로스 백"),
"가방-토트백" = c("토트백", "tote bag", "토트 백"),
"가방-클러치" = c("클러치", "clutch"),
"가방-백팩" = c("백팩", "backpack", "백 팩", "배낭"),
"가방-핸드백" = c("핸드백", "hand bag"),
"가방-버킷백" = c("버킷백", "bucket bag"),
"가방-호보백" = c("호보백", "hobo bag"),
"가방-기타" = c("가방", "백", "bag"),
# 지갑 세부 분류
"지갑-장지갑" = c("장지갑", "long wallet"),
"지갑-반지갑" = c("반지갑", "short wallet", "중지갑"),
"지갑-카드지갑" = c("카드지갑", "card wallet", "카드 지갑", "명함지갑"),
"지갑-동전지갑" = c("동전지갑", "coin wallet", "동전 지갑"),
"지갑-기타" = c("지갑", "월렛", "wallet"),
# 시계
"시계" = c("시계", "watch", "워치"),
# 신발 세부 분류
"신발-스니커즈" = c("스니커즈", "sneakers", "운동화"),
"신발-로퍼" = c("로퍼", "loafer"),
"신발-부츠" = c("부츠", "boots", "부트"),
"신발-샌들" = c("샌들", "sandal"),
"신발-슬리퍼" = c("슬리퍼", "slipper", "슬립온"),
"신발-펌프스" = c("펌프스", "pumps"),
"신발-기타" = c("신발", "슈즈", "shoes"),
# 벨트
"벨트" = c("벨트", "belt"),
# 의류 세부 분류
"의류-상의" = c("티셔츠", "셔츠", "블라우스", "후드", "니트", "맨투맨", "스웨터", "가디건"),
"의류-하의" = c("팬츠", "바지", "청바지", "진", "스커트", "반바지"),
"의류-아우터" = c("자켓", "코트", "점퍼", "패딩", "트렌치"),
"의류-원피스" = c("원피스", "dress", "드레스"),
# 액세서리 세부 분류
"액세서리-목걸이" = c("목걸이", "necklace", "펜던트"),
"액세서리-반지" = c("반지", "ring"),
"액세서리-귀걸이" = c("귀걸이", "earring", "이어링"),
"액세서리-팔찌" = c("팔찌", "bracelet"),
"액세서리-브로치" = c("브로치", "brooch"),
"액세서리-스카프" = c("스카프", "scarf", "머플러"),
"액세서리-선글라스" = c("선글라스", "sunglasses"),
"액세서리-모자" = c("모자", "hat", "cap", "캡"),
"액세서리-기타" = c("키링", "키홀더", "핸드폰케이스", "에어팟케이스")
)
# 품목 감지 함수 (세부 분류 우선)
detect_item <- function(title, content) {
text <- tolower(paste(title, content))
# 세부 분류부터 먼저 체크 (더 구체적인 것을 우선)
for (item in names(item_mapping)) {
keywords <- item_mapping[[item]]
for (keyword in keywords) {
if (str_detect(text, tolower(keyword))) {
return(item)
}
}
}
return("기타")
}
# 품목 컬럼 생성
cat("품목 분석 중...\n")
data$item <- mapply(detect_item, data$title, data$content)
# 대분류 추출 함수
get_major_category <- function(item) {
if (grepl("^가방-", item)) return("가방")
if (grepl("^지갑-", item)) return("지갑")
if (grepl("^신발-", item)) return("신발")
if (grepl("^의류-", item)) return("의류")
if (grepl("^액세서리-", item)) return("액세서리")
return(item)
}
data$item_major <- sapply(data$item, get_major_category)
# 가격대 분류 (나눔 포함)
data$price_range <- cut(
data$price_numeric,
breaks = c(-Inf, 0, 100000, 300000, 500000, 1000000, Inf),
labels = c("나눔/무료", "10만원 이하", "10-30만원", "30-50만원", "50-100만원", "100만원 이상"),
include.lowest = TRUE
)
# 나눔으로 표기된 것들은 price_range를 명시적으로 "나눔/무료"로 설정
data$price_range[data$is_naum] <- "나눔/무료"
# 카테고리 한글 매핑
category_names <- c(
"1" = "디지털기기",
"172" = "생활가전",
"8" = "가구/인테리어",
"7" = "생활/주방",
"4" = "유아동",
"173" = "유아도서",
"5" = "여성의류",
"31" = "여성잡화",
"14" = "남성패션/잡화",
"6" = "뷰티/미용",
"3" = "스포츠/레저",
"2" = "취미/게임/음반",
"9" = "도서",
"304" = "티켓/교환권",
"305" = "가공식품",
"483" = "건강기능식품",
"16" = "반려동물용품",
"139" = "식물",
"13" = "기타 중고물품",
"32" = "삽니다",
"516" = "GarbageBin(건강기능식품 복구용)"
)
# 카테고리명 컬럼 추가
data$category_name <- category_names[as.character(data$category_id)]
data$category_name[is.na(data$category_name)] <- paste0("카테고리 ", data$category_id[is.na(data$category_name)])
cat("데이터 전처리 완료!\n")
```
# 개요 {data-icon="fa-chart-line"}
## Column {data-width=350}
### 📊 데이터 기본 정보
```{r}
total_posts <- nrow(data)
total_users <- length(unique(data$user_id))
date_range <- paste(format(min(data$created_at, na.rm=TRUE), "%Y-%m-%d"), "~",
format(max(data$created_at, na.rm=TRUE), "%Y-%m-%d"))
avg_price <- mean(data$price_numeric[data$price_numeric > 0], na.rm = TRUE)
median_price <- median(data$price_numeric[data$price_numeric > 0], na.rm = TRUE)
info_df <- data.frame(
항목 = c("총 게시글 수", "총 사용자 수", "데이터 기간", "평균 가격", "중앙 가격"),
값 = c(
format(total_posts, big.mark = ","),
format(total_users, big.mark = ","),
date_range,
paste0(format(round(avg_price), big.mark = ","), "원"),
paste0(format(round(median_price), big.mark = ","), "원")
)
)
datatable(info_df,
options = list(dom = 't', pageLength = 10),
rownames = FALSE)
```
### 🏆 상위 검출 브랜드 (Top 20)
```{r}
brand_counts <- data %>%
filter(brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(20)
p <- plot_ly(brand_counts,
x = ~reorder(brand, -n),
y = ~n,
type = 'bar',
marker = list(color = '#3498db'),
text = ~paste0(n, "건"),
textposition = 'outside') %>%
layout(title = "",
xaxis = list(
title = "",
tickangle = -45,
tickfont = list(size = 11),
automargin = TRUE
),
yaxis = list(title = "게시글 수"),
margin = list(b = 120, l = 60),
height = 500)
p
```
## Column {data-width=350}
### 💰 가격대별 게시글 분포 (나눔 포함)
```{r}
price_dist <- data %>%
count(price_range) %>%
mutate(percentage = n / sum(n) * 100)
# 가격대 순서 정의
price_order <- c("10만원 이하", "10-30만원", "30-50만원", "50-100만원", "100만원 이상", "나눔/무료")
price_dist$price_range <- factor(price_dist$price_range, levels = rev(price_order))
price_dist <- price_dist %>% arrange(desc(price_range))
# 톤다운된 파스텔 색상
colors_map <- c("10만원 이하" = "#a8d5e2",
"10-30만원" = "#b8e6d5",
"30-50만원" = "#f9d5a7",
"50-100만원" = "#d5c4e8",
"100만원 이상" = "#f5b895",
"나눔/무료" = "#e8b4b8")
# 가로 막대 그래프
p <- plot_ly(price_dist,
y = ~price_range,
x = ~n,
type = 'bar',
orientation = 'h',
marker = list(color = colors_map[as.character(price_dist$price_range)]),
text = ~paste0(n, "건 (", round(percentage, 1), "%)"),
textposition = 'outside',
textfont = list(size = 13, color = '#333333'),
hovertemplate = paste(
'<b>%{y}</b><br>',
'건수: %{x:,}건<br>',
'비율: %{text}<br>',
'<extra></extra>'
)) %>%
layout(
title = "",
xaxis = list(
title = "게시글 수",
showgrid = TRUE,
range = c(0, 2500)
),
yaxis = list(title = "", tickfont = list(size = 13)),
margin = list(l = 120, r = 150),
height = 400,
showlegend = FALSE
)
p
```
### 📂 게시글 등록 카테고리별 분포 (Top 20)
```{r}
category_dist <- data %>%
count(category_id, sort = TRUE) %>%
head(20) %>%
mutate(category_label = ifelse(
as.character(category_id) %in% names(category_names),
category_names[as.character(category_id)],
paste0("카테고리 ", category_id)
))
p <- plot_ly(category_dist,
x = ~reorder(category_label, -n),
y = ~n,
type = 'bar',
marker = list(color = '#95a5a6'),
text = ~paste0(n, "건"),
textposition = 'outside') %>%
layout(title = "",
xaxis = list(
title = "",
tickangle = -45,
tickfont = list(size = 10),
automargin = TRUE
),
yaxis = list(title = "게시글 수"),
margin = list(b = 150),
height = 450)
p
```
# 브랜드 분석 {data-icon="fa-tags"}
## Column {data-width=500}
### 📈 브랜드별 등록 건수 (Top 20)
```{r}
brand_summary_chart <- data %>%
filter(brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(20) %>%
rename(총건수 = n)
p <- plot_ly(brand_summary_chart,
x = ~reorder(brand, -총건수),
y = ~총건수,
type = 'bar',
marker = list(
color = ~총건수,
colorscale = list(
c(0, '#e8f4f8'),
c(0.5, '#a8d5e2'),
c(1, '#6bb6d6')
),
showscale = FALSE
),
text = ~paste0(총건수, "건"),
textposition = 'outside',
textfont = list(size = 11, color = '#333333')) %>%
layout(
title = "",
xaxis = list(
title = "",
tickangle = -45,
tickfont = list(size = 11)
),
yaxis = list(title = "등록 건수"),
margin = list(b = 120),
height = 450
)
p
```
### 📊 브랜드별 평균 가격 (Top 20)
```{r}
brand_price <- data %>%
filter(brand != "기타" & !is.na(price_numeric) & price_numeric > 0) %>%
group_by(brand) %>%
summarise(
평균가격 = mean(price_numeric, na.rm = TRUE),
중앙가격 = median(price_numeric, na.rm = TRUE),
게시글수 = n()
) %>%
arrange(desc(평균가격)) %>%
head(20)
p <- plot_ly(brand_price,
x = ~reorder(brand, 평균가격),
y = ~평균가격,
type = 'bar',
name = '평균가격',
marker = list(color = '#e8b4b8')) %>%
add_trace(y = ~중앙가격,
name = '중앙가격',
marker = list(color = '#a8d5e2')) %>%
layout(title = "",
xaxis = list(title = "", tickangle = -45, tickfont = list(size = 10)),
yaxis = list(title = "가격 (원)"),
barmode = 'group',
margin = list(b = 120),
height = 450,
legend = list(orientation = 'h', y = 1.1))
p
```
## Column {data-width=500}
### 🏷️ 브랜드별 품목 분포 (Top 20)
```{r}
# 상위 20개 브랜드 선택
top_brands <- data %>%
filter(brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(20) %>%
pull(brand)
heatmap_data <- data %>%
filter(brand %in% top_brands & item_major != "기타") %>%
count(brand, item_major) %>%
pivot_wider(names_from = item_major, values_from = n, values_fill = 0)
# plotly 히트맵
heatmap_matrix <- as.matrix(heatmap_data[,-1])
rownames(heatmap_matrix) <- heatmap_data$brand
# 품목명 강제 추출
item_names <- colnames(heatmap_matrix)
# 파스텔 컬러 스케일 정의
pastel_colors <- list(
c(0, "rgb(255, 255, 255)"),
c(0.2, "rgb(230, 240, 255)"),
c(0.4, "rgb(200, 225, 255)"),
c(0.6, "rgb(170, 210, 255)"),
c(0.8, "rgb(140, 195, 255)"),
c(1, "rgb(110, 180, 245)")
)
p <- plot_ly(
x = item_names,
y = rownames(heatmap_matrix),
z = heatmap_matrix,
type = "heatmap",
colorscale = pastel_colors,
text = heatmap_matrix,
texttemplate = "%{z}",
textfont = list(size = 12, color = '#333333'),
showscale = TRUE,
colorbar = list(title = "건수")
) %>%
layout(
title = "",
xaxis = list(
title = list(text = "품목", font = list(size = 14, color = '#000000')),
tickfont = list(size = 13, color = '#000000'),
tickangle = 0,
showticklabels = TRUE,
tickmode = "array",
tickvals = seq(0, length(item_names)-1),
ticktext = item_names,
side = "bottom"
),
yaxis = list(
title = list(text = "브랜드", font = list(size = 14, color = '#000000')),
tickfont = list(size = 12, color = '#000000'),
autorange = "reversed",
showticklabels = TRUE
),
height = 500,
margin = list(l = 120, r = 100, t = 20, b = 80),
plot_bgcolor = 'white',
paper_bgcolor = 'white'
)
p
```
### 💎 고가 브랜드 분포 (50만원 이상)
```{r}
high_price_brand <- data %>%
filter(!is.na(price_numeric) & price_numeric >= 500000) %>%
count(brand) %>%
arrange(desc(n)) %>%
head(10)
pastel_palette <- c("#a8d5e2", "#b8e6d5", "#f9d5a7", "#d5c4e8",
"#f5b895", "#e8b4b8", "#c5e3f6", "#d4edda",
"#fff3cd", "#f8d7da")
p <- plot_ly(high_price_brand,
labels = ~brand,
values = ~n,
type = 'pie',
textinfo = 'label+percent',
marker = list(colors = pastel_palette[1:nrow(high_price_brand)]),
textfont = list(size = 11)) %>%
layout(title = "",
showlegend = TRUE,
height = 400)
p
```
# 상품 분석 {data-icon="fa-shopping-bag"}
## Column {data-width=600}
### 📦 품목별 등록 건수 (대분류)
```{r}
item_major_summary <- data %>%
filter(item_major != "기타") %>%
count(item_major, sort = TRUE)
p <- plot_ly(item_major_summary,
x = ~reorder(item_major, -n),
y = ~n,
type = 'bar',
marker = list(color = '#3498db'),
text = ~paste0(n, "건"),
textposition = 'outside') %>%
layout(title = "",
xaxis = list(title = "", tickfont = list(size = 12)),
yaxis = list(title = "게시글 수"),
margin = list(b = 80),
height = 400)
p
```
### 🔍 품목별 세부 분류 및 수량
```{r}
item_detail_summary <- data %>%
filter(item != "기타") %>%
count(item_major, item, sort = TRUE) %>%
arrange(item_major, desc(n)) %>%
rename(
대분류 = item_major,
세부분류 = item,
건수 = n
)
# 대분류별로 그룹화하여 표시
datatable(
item_detail_summary,
filter = 'top',
options = list(
pageLength = 25,
lengthMenu = c(10, 25, 50, 100),
dom = 'Blfrtip',
buttons = list('copy', 'csv', 'excel'),
columnDefs = list(
list(width = '150px', targets = 0),
list(width = '250px', targets = 1),
list(width = '100px', targets = 2),
list(className = 'dt-center', targets = 2)
),
scrollY = "400px",
scrollCollapse = TRUE,
order = list(list(0, 'asc'), list(2, 'desc'))
),
rownames = FALSE,
extensions = c('Buttons')
) %>%
formatStyle(
"대분류",
fontWeight = "bold",
backgroundColor = "#f8f9fa"
) %>%
formatStyle(
"건수",
fontWeight = "bold",
background = styleColorBar(range(item_detail_summary$건수), '#e8f4f8'),
backgroundSize = '80% 70%',
backgroundRepeat = 'no-repeat',
backgroundPosition = 'right'
)
```
## Column {data-width=400}
### 📊 세부 품목 Top 20
```{r}
top_items <- data %>%
filter(item != "기타") %>%
count(item, sort = TRUE) %>%
head(20)
p <- plot_ly(top_items,
y = ~reorder(item, n),
x = ~n,
type = 'bar',
orientation = 'h',
marker = list(color = '#9b59b6'),
text = ~paste0(n, "건"),
textposition = 'outside') %>%
layout(title = "",
xaxis = list(title = "건수"),
yaxis = list(title = ""),
margin = list(l = 150),
height = 500)
p
```
### 🎯 브랜드별 주요 세부 품목 (Top 10 브랜드)
```{r}
top_10_brands <- data %>%
filter(brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(10) %>%
pull(brand)
brand_item_detail <- data %>%
filter(brand %in% top_10_brands & item != "기타") %>%
count(brand, item, sort = TRUE) %>%
group_by(brand) %>%
slice_head(n = 3) %>%
ungroup() %>%
rename(
브랜드 = brand,
세부품목 = item,
건수 = n
)
datatable(
brand_item_detail,
filter = 'top',
options = list(
pageLength = 30,
scrollY = "350px",
dom = 'ftp'
),
rownames = FALSE
) %>%
formatStyle(
"브랜드",
fontWeight = "bold",
backgroundColor = "#fff3e0"
) %>%
formatStyle(
"건수",
fontWeight = "bold",
color = styleInterval(
cuts = c(10, 30, 50),
values = c("#6c757d", "#e67e22", "#e74c3c", "#c0392b")
)
)
```
# 주요 상품 분석 {data-icon="fa-star"}
## Column
### 🔍 브랜드별 주요 상품 (Top 20 브랜드)
```{r}
# 불필요한 단어 제거 함수
clean_product_name <- function(title, brand_name) {
brand_keywords <- unlist(brand_mapping[[brand_name]])
clean_title <- title
for (keyword in brand_keywords) {
clean_title <- str_replace_all(clean_title, paste0("(?i)", keyword), "")
}
if (brand_name == "셀린느") {
clean_title <- str_replace_all(clean_title, "느\\s", "")
clean_title <- str_replace_all(clean_title, "느$", "")
}
exclude_words <- c(
# 판매 관련
"팔아요", "팝니다", "판매", "팔아용", "파라요", "파라용", "팔아여", "팝니당", "팔게요", "팔아욤",
"급처", "급매", "급해요", "급합니다", "급하게", "빨리", "빨리요",
# 교환/나눔 관련
"네고", "교환", "나눔", "나눔요", "드려요", "줍니다", "드립니다",
# 상태 표현
"새상품", "새거", "새것", "새제품", "새물건", "미개봉", "미착용",
"정품", "미사용", "사용감", "사용안함", "안일어요", "안써요",
"중고", "중고품", "거의새것", "거의안", "거의", "사용했어요",
# 외관/상태 형용사
"깨끗", "깨끗해요", "깨끗한", "깔끔", "깔끔해요", "깔끔한",
"예쁜", "이쁜", "예뻐요", "이뻐요", "예쁨", "이쁨",
"좋아요", "좋은", "좋음", "양호", "최상", "상태좋음", "상태양호",
"완전", "진짜", "정말", "너무", "엄청", "매우", "아주",
# 가격 관련
"싸게", "싸요", "저렴", "저렴해요", "저렴하게", "할인", "세일",
"비싸요", "비싼", "고가", "가격", "만원", "천원",
# 거래 방식
"연락", "문의", "직거래", "택배", "배송", "편택", "반택",
"입니다", "해요", "해용", "합니다", "있어요", "없어요", "있음", "없음",
"주세요", "사세요", "연락주세요", "문의주세요",
# 기타 불필요한 표현
"상태", "정도", "느낌", "색상", "컬러", "색깔",
"사이즈", "싸이즈", "크기", "치수",
"구매", "구입", "샀어요", "샀습니다",
"하나", "한개", "개", "장", "개입",
"이거", "이것", "저거", "저것", "요거", "요것",
"여기", "저기", "요기",
# 추가 동사 변형
"해드려요", "드려용", "팔아볼게요", "내놔요", "올려요", "올립니다",
"가져가세요", "가져가요", "양도", "양도해요",
# 감탄사/추임새
"완전", "대박", "진심", "레알", "찐",
"ㅠㅠ", "ㅜㅜ", "ㅎㅎ", "ㅋㅋ"
)
for (word in exclude_words) {
clean_title <- str_replace_all(clean_title, paste0("(?i)", word), "")
}
# 특수문자 및 숫자 제거 (품목명만 남김)
clean_title <- str_replace_all(clean_title, "[^가-힣a-zA-Z\\s]", " ")
clean_title <- str_squish(clean_title)
clean_title <- str_trim(clean_title)
# 너무 짧은 경우 제외 (2글자 미만)
if (nchar(clean_title) < 2) {
return(NA)
}
# 단일 글자가 반복되는 경우 제외 (예: "ㅋㅋㅋ")
if (str_detect(clean_title, "^(.)\\1+$")) {
return(NA)
}
return(clean_title)
}
# 상위 20개 브랜드의 주요 상품 5개씩
top_20_brands <- data %>%
filter(brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(20) %>%
pull(brand)
product_summary_all <- data.frame()
for (brand_name in top_20_brands) {
total_count <- data %>%
filter(brand == brand_name) %>%
nrow()
brand_data <- data %>%
filter(brand == brand_name) %>%
select(title)
top_products <- brand_data %>%
mutate(clean_product = sapply(title, function(x) clean_product_name(x, brand_name))) %>%
filter(!is.na(clean_product) & clean_product != "") %>%
count(clean_product, sort = TRUE) %>%
head(5)
if (nrow(top_products) > 0) {
for (i in 1:nrow(top_products)) {
product_summary_all <- rbind(product_summary_all, data.frame(
브랜드 = brand_name,
총건수 = total_count,
순위 = i,
상품명 = top_products$clean_product[i],
건수 = top_products$n[i],
비율 = paste0(round(top_products$n[i] / total_count * 100, 1), "%"),
stringsAsFactors = FALSE
))
}
}
}
# 개선된 DT 테이블 - 각 컬럼별 개별 검색 기능
datatable(
product_summary_all,
# 필터 옵션 추가
filter = 'top',
# 확장된 옵션
options = list(
pageLength = 20,
lengthMenu = c(10, 20, 50, 100),
dom = 'Blfrtip',
buttons = list(
'copy',
'csv',
'excel',
list(
extend = 'colvis',
text = '컬럼 선택'
)
),
# 컬럼 정의
columnDefs = list(
list(width = '100px', targets = c(0, 1, 2, 4, 5)),
list(width = '250px', targets = 3),
list(className = 'dt-center', targets = c(1, 2, 4, 5)),
list(
targets = 0,
render = JS(
"function(data, type, row, meta) {",
"if(type === 'display'){",
"return '<span style=\"background-color: #f8f9fa; padding: 2px 6px; border-radius: 3px; font-weight: bold;\">' + data + '</span>';",
"} else {",
"return data;",
"}",
"}"
)
)
),
# 스크롤 설정
scrollY = "500px",
scrollCollapse = TRUE,
# 정렬 설정
order = list(list(1, 'desc'), list(0, 'asc')),
# 언어 설정
language = list(
lengthMenu = "페이지당 _MENU_ 개",
info = "_START_-_END_ / 총 _TOTAL_개",
infoEmpty = "데이터 없음",
infoFiltered = "(전체 _MAX_개 중 검색됨)",
search = "검색:"
)
),
# 기본 설정
rownames = FALSE,
extensions = c('Buttons', 'ColReorder', 'FixedHeader'),
escape = FALSE
) %>%
# 스타일링
formatStyle(
"비율",
backgroundColor = styleInterval(
cuts = c(5, 10, 15, 25),
values = c("#f8f9fa", "#e3f2fd", "#fff3e0", "#ffecb3", "#ffcdd2")
),
fontWeight = styleInterval(
cuts = c(15),
values = c("normal", "bold")
)
) %>%
formatStyle(
"건수",
fontWeight = "bold",
color = styleInterval(
cuts = c(10, 30, 50, 100),
values = c("#6c757d", "#495057", "#333333", "#dc3545", "#cc0000")
)
) %>%
formatStyle(
"브랜드",
fontWeight = "bold"
) %>%
formatStyle(
"총건수",
background = styleColorBar(range(product_summary_all$총건수), '#e8f4f8'),
backgroundSize = '80% 70%',
backgroundRepeat = 'no-repeat',
backgroundPosition = 'right'
)
```
# 키워드 분석 {data-icon="fa-key"}
## Column {data-width=600}
### 📊 가품 의심 키워드 카테고리별 빈도
```{r}
# 가품 탐지 키워드 카테고리 정의 (유사 단어 그룹화)
counterfeit_keywords <- list(
"가품/레플리카" = c("가품", "짭", "짭퉁", "직퉁", "이미테이션", "이미제품", "이미상품", "레플", "레플리카", "위조품", "위조상품", "카피", "카피샵", "비정품"),
"등급 표현" = c("미러급", "미러퀄", "S급", "A급", "AA급", "AAA급", "에이급", "에이에이", "에스급", "백화점급", "백화점퀄", "하이엔드급", "에이뜹"),
"제작 관련" = c("1:1제작", "일대일제작", "일대일 제작", "국내제작", "자체제작", "맞춤제작", "맞춤", "제작", "1:1", "100%", "동일", "비교불가"),
"공장명" = c("ar공장", "gt공장", "h12공장", "OG공장", "PK공장", "top공장", "vr공장", "will공장", "XF공장", "ZH공장", "중국공장"),
"정품 불확실" = c("정품 몰라", "정품 모르", "답 안해", "답안해", "대답 안해", "대답안해", "확인 안해", "확인안해", "확실치않", "확실치 않", "확실하지않", "확실하지 않", "정가품 여부", "정품여부", "정품 여부", "물어보지마", "물어 보지마", "안받아요", "안 받아요"),
"정품 부정 표현" = c("정품 아니", "정품 아닙", "정품아니", "정품아닙", "정품 아님", "ㅈㅍ 아니", "ㅈㅍ 아니", "ㅈ품 아니", "정제품 아니", "정 아닌", "정 아닐", "정/가", "정.가"),
"노벨티/기프트" = c("노벨티", "노벨트", "노 벨 티", "vip기프트", "vip 기프트", "업사이클"),
"택/라벨 없음" = c("택 없", "택없", "케어라벨 없", "케어라밸 없", "캐어라벨 없", "라벨 없", "라벨없", "보증서 없", "보증서없"),
"의심 쇼핑몰" = c("판더샵", "단풍샵", "지존샵", "보세", "동대문"),
"거래 조건" = c("택배 거래", "비대면 거래", "직거래만", "단순변심교환환불안받습니다", "가격대비 편하게 써주실분만요", "구매처 확인 불가"),
"상태/출처" = c("새상품", "새제품", "새 거", "미개봉", "풀박스", "선물 받았다", "선물받았다"),
"기타 의심 표현" = c("정과 같", "정과 동일", "정급", "정문의x", "찐문의", "가격 보면 아시죠")
)
# 키워드 빈도 계산 함수
count_keywords <- function(text_vector, keywords) {
total_count <- 0
for (keyword in keywords) {
count <- sum(str_count(text_vector, paste0("(?i)", keyword)))
total_count <- total_count + count
}
return(total_count)
}
# 카테고리별 빈도 계산
counterfeit_category_freq <- data.frame(
카테고리 = character(),
빈도 = numeric(),
stringsAsFactors = FALSE
)
for (category_name in names(counterfeit_keywords)) {
keywords <- counterfeit_keywords[[category_name]]
freq <- count_keywords(paste(data$title, data$content), keywords)
counterfeit_category_freq <- rbind(counterfeit_category_freq, data.frame(
카테고리 = category_name,
빈도 = freq
))
}
# 카테고리별 빈도 차트
p <- plot_ly(counterfeit_category_freq,
x = ~reorder(카테고리, 빈도),
y = ~빈도,
type = 'bar',
marker = list(
color = ~빈도,
colorscale = list(
c(0, '#fff3e0'),
c(0.5, '#ff9800'),
c(1, '#e65100')
),
showscale = FALSE
),
text = ~paste0(format(빈도, big.mark = ","), "회"),
textposition = 'outside',
textfont = list(size = 12)) %>%
layout(
title = "",
xaxis = list(title = "", tickangle = -45),
yaxis = list(title = "출현 빈도"),
margin = list(b = 120)
)
p
```
### 🔍 가품 의심 주요 키워드 Top 50
```{r}
# 각 키워드별 빈도 계산
counterfeit_keyword_freq <- data.frame(
키워드 = character(),
빈도 = numeric(),
카테고리 = character(),
stringsAsFactors = FALSE
)
for (category_name in names(counterfeit_keywords)) {
keywords <- counterfeit_keywords[[category_name]]
for (keyword in keywords) {
freq <- sum(str_count(paste(data$title, data$content), paste0("(?i)", keyword)))
if (freq > 0) {
counterfeit_keyword_freq <- rbind(counterfeit_keyword_freq, data.frame(
키워드 = keyword,
빈도 = freq,
카테고리 = category_name
))
}
}
}
# 상위 50개 선택
top_counterfeit_keywords <- counterfeit_keyword_freq %>%
arrange(desc(빈도)) %>%
head(50)
# 카테고리별 색상
counterfeit_category_colors <- c(
"가품/레플리카" = "#e74c3c",
"등급 표현" = "#e67e22",
"제작 관련" = "#f39c12",
"공장명" = "#d35400",
"정품 불확실" = "#c0392b",
"정품 부정 표현" = "#a93226",
"노벨티/기프트" = "#ff9800",
"택/라벨 없음" = "#ff5722",
"의심 쇼핑몰" = "#bf360c",
"거래 조건" = "#d84315",
"상태/출처" = "#ff6f00",
"기타 의심 표현" = "#dd2c00"
)
top_counterfeit_keywords$색상 <- counterfeit_category_colors[top_counterfeit_keywords$카테고리]
p <- plot_ly(top_counterfeit_keywords,
y = ~reorder(키워드, 빈도),
x = ~빈도,
type = 'bar',
orientation = 'h',
marker = list(color = ~색상),
text = ~paste0(format(빈도, big.mark = ","), "회"),
textposition = 'outside',
hovertemplate = paste(
'<b>%{y}</b><br>',
'빈도: %{x:,}회<br>',
'카테고리: ', top_counterfeit_keywords$카테고리, '<br>',
'<extra></extra>'
)) %>%
layout(
title = "",
xaxis = list(title = "출현 빈도"),
yaxis = list(title = ""),
margin = list(l = 150),
showlegend = FALSE,
height = 900
)
p
```
## Column {data-width=400}
### 📈 가품 키워드 카테고리별 비율
```{r}
counterfeit_category_freq <- counterfeit_category_freq %>%
mutate(비율 = round(빈도 / sum(빈도) * 100, 1))
p <- plot_ly(counterfeit_category_freq,
labels = ~카테고리,
values = ~빈도,
type = 'pie',
textinfo = 'label+percent',
marker = list(
colors = c('#e74c3c', '#e67e22', '#f39c12', '#d35400', '#c0392b', '#a93226',
'#ff9800', '#ff5722', '#bf360c', '#d84315', '#ff6f00', '#dd2c00')
),
textfont = list(size = 11)) %>%
layout(title = "",
showlegend = TRUE,
height = 400)
p
```
### 📋 가품 키워드 상세 통계
```{r}
counterfeit_summary <- counterfeit_category_freq %>%
arrange(desc(빈도)) %>%
mutate(
비율 = paste0(비율, "%"),
빈도 = format(빈도, big.mark = ",")
)
datatable(counterfeit_summary,
options = list(
dom = 't',
pageLength = 10
),
rownames = FALSE) %>%
formatStyle(
"빈도",
fontWeight = "bold",
backgroundColor = "#fff3e0"
)
```
### 🎯 위험도 높은 게시글 비율
```{r}
# 가품 의심 키워드가 포함된 게시글 수 계산
all_counterfeit_keywords <- unlist(counterfeit_keywords)
data$has_counterfeit_keyword <- sapply(1:nrow(data), function(i) {
text <- paste(tolower(data$title[i]), tolower(data$content[i]))
any(sapply(all_counterfeit_keywords, function(kw) {
str_detect(text, paste0("(?i)", kw))
}))
})
risk_summary <- data.frame(
구분 = c("가품 키워드 포함", "가품 키워드 미포함", "전체 게시글"),
게시글수 = c(
sum(data$has_counterfeit_keyword),
sum(!data$has_counterfeit_keyword),
nrow(data)
)
) %>%
mutate(
비율 = paste0(round(게시글수 / nrow(data) * 100, 1), "%"),
게시글수 = format(게시글수, big.mark = ",")
)
datatable(risk_summary,
options = list(
dom = 't',
pageLength = 5
),
rownames = FALSE) %>%
formatStyle(
"구분",
fontWeight = "bold"
) %>%
formatStyle(
"게시글수",
fontWeight = "bold",
color = styleEqual(
c(risk_summary$게시글수[1]),
c("#e74c3c")
)
)
```
### 🔎 카테고리별 주요 키워드 (Top 3)
```{r}
# 각 카테고리의 상위 3개 키워드
counterfeit_top_keywords <- counterfeit_keyword_freq %>%
group_by(카테고리) %>%
arrange(desc(빈도)) %>%
slice(1:3) %>%
ungroup() %>%
mutate(순위 = rep(1:3, length.out = n()))
datatable(counterfeit_top_keywords %>%
select(카테고리, 순위, 키워드, 빈도) %>%
mutate(빈도 = format(빈도, big.mark = ",")),
filter = 'top',
options = list(
pageLength = 36,
scrollY = "350px",
dom = 'ftp'
),
rownames = FALSE) %>%
formatStyle(
"빈도",
fontWeight = "bold",
color = styleInterval(
cuts = c(10, 50, 100),
values = c("#6c757d", "#e67e22", "#e74c3c", "#c0392b")
)
) %>%
formatStyle(
"카테고리",
backgroundColor = "#fff3e0"
)
```
# 신고자 분석 {data-icon="fa-user"}
## Column {data-width=600}
### 🚨 신고 다발 사용자 (Top 20)
```{r}
user_violations <- data %>%
count(user_id, sort = TRUE) %>%
head(20) %>%
mutate(user_label = paste0("User ", user_id))
p <- plot_ly(user_violations,
x = ~reorder(user_label, n),
y = ~n,
type = 'bar',
marker = list(color = ~n,
colorscale = list(c(0, '#3498db'), c(1, '#e74c3c')),
showscale = TRUE),
text = ~paste0(n, "건"),
textposition = 'outside') %>%
layout(title = "",
xaxis = list(title = ""),
yaxis = list(title = "신고 건수"),
margin = list(b = 150))
p
```
### 📈 신고 빈도 분포
```{r}
violation_freq <- data %>%
count(user_id) %>%
mutate(frequency_group = case_when(
n == 1 ~ "1회",
n == 2 ~ "2회",
n >= 3 & n < 5 ~ "3-4회",
n >= 5 & n < 10 ~ "5-9회",
n >= 10 & n < 20 ~ "10-19회",
n >= 20 ~ "20회 이상"
)) %>%
count(frequency_group) %>%
mutate(frequency_group = factor(frequency_group,
levels = c("1회", "2회", "3-4회", "5-9회", "10-19회", "20회 이상")))
p <- plot_ly(violation_freq,
x = ~frequency_group,
y = ~n,
type = 'bar',
marker = list(color = '#9b59b6'),
text = ~paste0(n, "명"),
textposition = 'outside') %>%
layout(title = "사용자별 신고 횟수 분포",
xaxis = list(title = "신고 횟수"),
yaxis = list(title = "사용자 수"))
p
```
## Column {data-width=400}
### 👤 신고 통계 요약
```{r}
user_stats <- data %>%
count(user_id) %>%
summarise(
`총 사용자 수` = n(),
`1회 신고` = sum(n == 1),
`2회 이상` = sum(n >= 2),
`5회 이상` = sum(n >= 5),
`10회 이상` = sum(n >= 10),
`20회 이상` = sum(n >= 20)
) %>%
pivot_longer(everything(), names_to = "구분", values_to = "인원수")
datatable(user_stats,
options = list(dom = 't', pageLength = 10),
rownames = FALSE)
```
### 🎯 다발 신고자의 주요 브랜드
```{r}
frequent_violators <- data %>%
count(user_id) %>%
filter(n >= 5) %>%
pull(user_id)
frequent_brands <- data %>%
filter(user_id %in% frequent_violators & brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(10)
p <- plot_ly(frequent_brands,
labels = ~brand,
values = ~n,
type = 'pie',
textinfo = 'label+value',
marker = list(colors = RColorBrewer::brewer.pal(min(10, nrow(frequent_brands)), "Paired"))) %>%
layout(title = "다발 신고자(5회+)의 브랜드 분포")
p
```
### ⚠️ 고위험 사용자 상세 (Top 10)
```{r}
user_violations_top <- data %>%
count(user_id, sort = TRUE) %>%
head(10)
high_risk_users <- data %>%
filter(user_id %in% user_violations_top$user_id) %>%
group_by(user_id) %>%
summarise(
신고건수 = n(),
주요브랜드 = names(sort(table(brand), decreasing = TRUE))[1],
평균가격 = mean(price_numeric[price_numeric > 0], na.rm = TRUE),
최고가격 = max(price_numeric, na.rm = TRUE)
) %>%
arrange(desc(신고건수)) %>%
mutate(
평균가격 = paste0(format(round(평균가격), big.mark = ","), "원"),
최고가격 = paste0(format(round(최고가격), big.mark = ","), "원")
) %>%
rename(사용자ID = user_id)
datatable(high_risk_users,
options = list(pageLength = 10, dom = 'tip'),
rownames = FALSE)
```
# 시계열 분석 {data-icon="fa-calendar"}
## Column {data-width=600}
### 📅 월별 신고 건수 추이
```{r}
monthly_trend <- data %>%
mutate(year_month = format(created_at, "%Y-%m")) %>%
count(year_month) %>%
arrange(year_month)
p <- plot_ly(monthly_trend,
x = ~year_month,
y = ~n,
type = 'scatter',
mode = 'lines+markers',
line = list(color = '#3498db', width = 2),
marker = list(size = 6)) %>%
layout(title = "",
xaxis = list(title = "월", tickangle = -45),
yaxis = list(title = "신고 건수"))
p
```
### 🔥 브랜드별 시계열 트렌드 (Top 5)
```{r}
top5_brands <- data %>%
filter(brand != "기타") %>%
count(brand, sort = TRUE) %>%
head(5) %>%
pull(brand)
brand_monthly <- data %>%
filter(brand %in% top5_brands) %>%
mutate(year_month = format(created_at, "%Y-%m")) %>%
count(year_month, brand) %>%
arrange(year_month)
p <- plot_ly()
for(b in top5_brands) {
brand_data <- brand_monthly %>% filter(brand == b)
p <- p %>% add_trace(
data = brand_data,
x = ~year_month,
y = ~n,
name = b,
type = 'scatter',
mode = 'lines+markers'
)
}
p <- p %>% layout(
title = "",
xaxis = list(title = "월", tickangle = -45),
yaxis = list(title = "신고 건수")
)
p
```
## Column {data-width=400}
### ⏰ 시간대별 게시 패턴
```{r}
hourly_pattern <- data %>%
mutate(hour = as.numeric(format(created_at, "%H"))) %>%
count(hour) %>%
arrange(hour)
p <- plot_ly(hourly_pattern,
x = ~hour,
y = ~n,
type = 'scatter',
mode = 'lines+markers',
fill = 'tozeroy',
line = list(color = '#9b59b6'),
marker = list(color = '#9b59b6')) %>%
layout(title = "",
xaxis = list(title = "시간 (0-23시)", dtick = 2),
yaxis = list(title = "게시글 수"))
p
```
# 데이터 테이블 {data-icon="fa-table"}
## Column
### 📋 전체 데이터 (필터링 가능)
```{r}
display_data <- data %>%
select(id, title, content, brand, item_major, item, price_numeric, user_id, created_at, category_name) %>%
rename(
게시글ID = id,
제목 = title,
내용 = content,
브랜드 = brand,
품목대분류 = item_major,
품목세부 = item,
가격 = price_numeric,
사용자ID = user_id,
생성일 = created_at,
카테고리 = category_name
) %>%
mutate(
내용 = ifelse(nchar(내용) > 150, paste0(substr(내용, 1, 150), "..."), 내용),
가격 = ifelse(!is.na(가격) & 가격 > 0, paste0(format(round(가격), big.mark = ","), "원"), "나눔/무료"),
생성일 = format(생성일, "%Y-%m-%d %H:%M")
)
datatable(display_data,
filter = 'top',
options = list(
pageLength = 25,
scrollX = TRUE,
autoWidth = TRUE
),
rownames = FALSE)
```