11import { DataSource } from 'typeorm' ;
22import * as winston from 'winston' ;
33import { ulid } from 'ulid' ;
4+ import * as starvingOrange from 'starving-orange' ;
45import {
56 District ,
67 Category ,
@@ -11,10 +12,14 @@ import {
1112 UserProfile ,
1213 UserRewards ,
1314 Mission ,
15+ Meeting ,
16+ MeetingParticipant ,
1417} from '../../entities' ;
1518import { UserStatus } from '../../entities/user.entity' ;
1619import { Gender } from '../../entities/user-profile.entity' ;
1720import { MissionDifficulty } from '../../entities/mission.entity' ;
21+ import { MeetingStatus } from '../../entities/meeting.entity' ;
22+ import { ParticipantStatus } from '../../entities/meeting-participant.entity' ;
1823
1924export const seedInitialData = async ( dataSource : DataSource ) => {
2025 const logger = winston . createLogger ( {
@@ -37,6 +42,8 @@ export const seedInitialData = async (dataSource: DataSource) => {
3742 const userProfileRepository = dataSource . getRepository ( UserProfile ) ;
3843 const userRewardsRepository = dataSource . getRepository ( UserRewards ) ;
3944 const missionRepository = dataSource . getRepository ( Mission ) ;
45+ const meetingRepository = dataSource . getRepository ( Meeting ) ;
46+ const participantRepository = dataSource . getRepository ( MeetingParticipant ) ;
4047
4148 // 서울 구 데이터 시딩
4249 const seoulDistricts = [
@@ -337,7 +344,8 @@ export const seedInitialData = async (dataSource: DataSource) => {
337344 {
338345 id : '01JG9H7E2FQMC8GN1VKXR6W3T9' ,
339346 title : '송파구 맛집 방문하기' ,
340- description : '송파구 내 인기 맛집을 방문하고 인증 사진을 업로드하세요.' ,
347+ description :
348+ '송파구 내 인기 맛집을 방문하고 인증 사진을 업로드해 주세요.' ,
341349 basePoints : 500 ,
342350 estimatedDuration : 120 ,
343351 minParticipants : 4 ,
@@ -690,5 +698,237 @@ export const seedInitialData = async (dataSource: DataSource) => {
690698 }
691699 }
692700
701+ // 20명의 더미 사용자 생성
702+ logger . info ( '👥 20명의 더미 사용자 생성 시작...' ) ;
703+
704+ const activeSongpaDistrict = await districtRepository . findOne ( {
705+ where : { districtName : '송파구' , isActive : true } ,
706+ } ) ;
707+
708+ const allInterests = await userInterestsRepository . find ( ) ;
709+ const allHashtags = await userHashtagsRepository . find ( ) ;
710+
711+ if (
712+ activeSongpaDistrict &&
713+ allInterests . length > 0 &&
714+ allHashtags . length > 0
715+ ) {
716+ // 010######## 형식의 랜덤 번호 20개 생성
717+ const phoneNumbers : string [ ] = [ ] ;
718+ const phoneSet = new Set < string > ( ) ;
719+ while ( phoneNumbers . length < 20 ) {
720+ const randomNumber = Math . floor ( Math . random ( ) * 1_0000_0000 )
721+ . toString ( )
722+ . padStart ( 8 , '0' ) ;
723+ const phone = `010${ randomNumber } ` ;
724+ if ( ! phoneSet . has ( phone ) ) {
725+ phoneNumbers . push ( phone ) ;
726+ phoneSet . add ( phone ) ;
727+ }
728+ }
729+
730+ const genders = [ Gender . MALE , Gender . FEMALE , Gender . OTHER , null ] ;
731+
732+ // Type-safe random gender selection helper
733+ const getRandomGender = ( ) : Gender | null => {
734+ const randomIndex = Math . floor ( Math . random ( ) * genders . length ) ;
735+ return genders [ randomIndex ] ?? null ;
736+ } ;
737+
738+ const mbtiTypes = [
739+ 'INFP' ,
740+ 'ENFP' ,
741+ 'INFJ' ,
742+ 'ENFJ' ,
743+ 'INTJ' ,
744+ 'ENTJ' ,
745+ 'INTP' ,
746+ 'ENTP' ,
747+ 'ISFP' ,
748+ 'ESFP' ,
749+ 'ISTP' ,
750+ 'ESTP' ,
751+ 'ISFJ' ,
752+ 'ESFJ' ,
753+ 'ISTJ' ,
754+ 'ESTJ' ,
755+ ] ;
756+
757+ for ( let i = 0 ; i < 20 ; i ++ ) {
758+ const phoneNumber = phoneNumbers [ i ] ;
759+
760+ // 이미 존재하는 사용자인지 확인
761+ const existingUser = await userRepository . findOne ( {
762+ where : { phoneNumber } ,
763+ } ) ;
764+
765+ if ( existingUser ) {
766+ continue ; // 이미 존재하면 건너뜀
767+ }
768+
769+ const userId = ulid ( ) ;
770+ const nicknameResult = starvingOrange . generateNickname ( ) ;
771+ const nickname = nicknameResult . nickname ;
772+
773+ // 랜덤한 관심사 2-4개 선택
774+ const interestCount = Math . floor ( Math . random ( ) * 3 ) + 2 ;
775+ const randomInterests = allInterests
776+ . sort ( ( ) => 0.5 - Math . random ( ) )
777+ . slice ( 0 , interestCount )
778+ . map ( ( interest ) => interest . id ) ;
779+
780+ // 랜덤한 해시태그 1-3개 선택
781+ const hashtagCount = Math . floor ( Math . random ( ) * 3 ) + 1 ;
782+ const randomHashtags = allHashtags
783+ . sort ( ( ) => 0.5 - Math . random ( ) )
784+ . slice ( 0 , hashtagCount )
785+ . map ( ( hashtag ) => hashtag . id ) ;
786+
787+ // 사용자 생성
788+ const user = userRepository . create ( {
789+ id : userId ,
790+ phoneNumber,
791+ phoneVerifiedAt : new Date ( ) ,
792+ onboardingCompletedAt : new Date ( ) ,
793+ lastLoginAt : new Date (
794+ Date . now ( ) - Math . random ( ) * 30 * 24 * 60 * 60 * 1000 ,
795+ ) , // 지난 30일 내 랜덤
796+ status : UserStatus . ACTIVE ,
797+ } ) ;
798+ await userRepository . save ( user ) ;
799+
800+ // 사용자 프로필 생성
801+ const profile = userProfileRepository . create ( {
802+ userId,
803+ nickname,
804+ profileImageUrl : `https://api.dicebear.com/7.x/avataaars/svg?seed=${ nickname } ` ,
805+ bio : `안녕하세요! ${ nickname } 입니다. 새로운 사람들과 함께 재미있는 활동을 해보고 싶어요!` ,
806+ birthYear : Math . floor ( Math . random ( ) * 25 ) + 1990 , // 1990-2014
807+ gender : getRandomGender ( ) ,
808+ mbti : mbtiTypes [ Math . floor ( Math . random ( ) * mbtiTypes . length ) ] ,
809+ interestIds : randomInterests ,
810+ hashtagIds : randomHashtags ,
811+ districtId : activeSongpaDistrict . id ,
812+ points : Math . floor ( Math . random ( ) * 2000 ) , // 0-1999 포인트
813+ } ) ;
814+ await userProfileRepository . save ( profile ) ;
815+
816+ // 사용자 보상 생성
817+ const rewards = userRewardsRepository . create ( {
818+ userId,
819+ aiMissionTickets : Math . floor ( Math . random ( ) * 5 ) , // 0-4 티켓
820+ } ) ;
821+ await userRewardsRepository . save ( rewards ) ;
822+ }
823+ }
824+
825+ // 이번 주 모임 더미데이터 생성
826+ const allMissions = await missionRepository . find ( {
827+ where : { isActive : true } ,
828+ } ) ;
829+ const allUsers = await userRepository . find ( {
830+ relations : [ 'profile' ] ,
831+ where : { status : UserStatus . ACTIVE } ,
832+ } ) ;
833+
834+ if ( allMissions . length > 0 && allUsers . length > 0 ) {
835+ const now = new Date ( ) ;
836+ const weekStart = new Date ( now ) ;
837+ weekStart . setDate ( now . getDate ( ) - now . getDay ( ) + 1 ) ; // 이번 주 월요일
838+ weekStart . setHours ( 0 , 0 , 0 , 0 ) ;
839+
840+ // 이번 주 각 날짜별로 1-3개의 모임 생성
841+ for ( let day = 0 ; day < 7 ; day ++ ) {
842+ const meetingDate = new Date ( weekStart ) ;
843+ meetingDate . setDate ( weekStart . getDate ( ) + day ) ;
844+
845+ // 과거 날짜는 건너뛰기
846+ if ( meetingDate < now ) {
847+ continue ;
848+ }
849+
850+ const meetingsToday = Math . floor ( Math . random ( ) * 3 ) + 1 ; // 1-3개
851+
852+ for ( let i = 0 ; i < meetingsToday ; i ++ ) {
853+ const mission =
854+ allMissions [ Math . floor ( Math . random ( ) * allMissions . length ) ] ;
855+ const host = allUsers [ Math . floor ( Math . random ( ) * allUsers . length ) ] ;
856+
857+ // 모임 시작 시간 (10:00 ~ 20:00, 30분 단위)
858+ const startHour = Math . floor ( Math . random ( ) * 11 ) + 10 ; // 10-20시
859+ const startMinute = Math . random ( ) < 0.5 ? 0 : 30 ; // 0분 또는 30분
860+ const scheduledAt = new Date ( meetingDate ) ;
861+ scheduledAt . setHours ( startHour , startMinute , 0 , 0 ) ;
862+
863+ // 모집 마감시간 (모임 시작 2-24시간 전)
864+ const recruitHoursBefore = Math . floor ( Math . random ( ) * 23 ) + 2 ; // 2-24시간 전
865+ const recruitUntil = new Date ( scheduledAt ) ;
866+ recruitUntil . setHours ( scheduledAt . getHours ( ) - recruitHoursBefore ) ;
867+
868+ // 모임이 이미 모집 마감되었는지 확인
869+ const isRecruitmentClosed = recruitUntil < now ;
870+
871+ const meetingId = ulid ( ) ;
872+
873+ const meeting = meetingRepository . create ( {
874+ id : meetingId ,
875+ missionId : mission . id ,
876+ hostUserId : host . id ,
877+ status : isRecruitmentClosed
878+ ? MeetingStatus . ACTIVE
879+ : MeetingStatus . RECRUITING ,
880+ recruitUntil,
881+ scheduledAt,
882+ qrCodeToken : isRecruitmentClosed
883+ ? `qr_${ ulid ( ) . substring ( 0 , 10 ) } `
884+ : null ,
885+ qrGeneratedAt : isRecruitmentClosed ? new Date ( ) : null ,
886+ } ) ;
887+
888+ await meetingRepository . save ( meeting ) ;
889+
890+ // 호스트를 참가자로 추가
891+ const hostParticipant = participantRepository . create ( {
892+ meetingId,
893+ userId : host . id ,
894+ isHost : true ,
895+ status : ParticipantStatus . JOINED ,
896+ joinedAt : new Date (
897+ meeting . createdAt . getTime ( ) + Math . random ( ) * 60000 ,
898+ ) , // 생성 후 1분 이내
899+ } ) ;
900+ await participantRepository . save ( hostParticipant ) ;
901+
902+ // 추가 참가자들 (1-3명)
903+ const additionalParticipants = Math . floor ( Math . random ( ) * 3 ) + 1 ;
904+ const usedUsers = new Set ( [ host . id ] ) ;
905+
906+ for (
907+ let j = 0 ;
908+ j < additionalParticipants && usedUsers . size < allUsers . length ;
909+ j ++
910+ ) {
911+ let randomUser : ( typeof allUsers ) [ 0 ] ;
912+ do {
913+ randomUser = allUsers [ Math . floor ( Math . random ( ) * allUsers . length ) ] ;
914+ } while ( usedUsers . has ( randomUser . id ) ) ;
915+
916+ usedUsers . add ( randomUser . id ) ;
917+
918+ const participant = participantRepository . create ( {
919+ meetingId,
920+ userId : randomUser . id ,
921+ isHost : false ,
922+ status : ParticipantStatus . JOINED ,
923+ joinedAt : new Date (
924+ meeting . createdAt . getTime ( ) + Math . random ( ) * 24 * 60 * 60 * 1000 ,
925+ ) , // 24시간 이내
926+ } ) ;
927+ await participantRepository . save ( participant ) ;
928+ }
929+ }
930+ }
931+ }
932+
693933 logger . info ( '🌱 Initial data seeding has been completed.' ) ;
694934} ;
0 commit comments