diff --git a/cypress/e2e/Integration/LoginPage.cy.ts b/cypress/e2e/Integration/LoginPage.cy.ts index 0bd616c..6231147 100644 --- a/cypress/e2e/Integration/LoginPage.cy.ts +++ b/cypress/e2e/Integration/LoginPage.cy.ts @@ -1,6 +1,4 @@ -import { login } from '@/apis/PromotionAdmin/login'; import { MSG } from '@/constants/messages'; -import { PA_ROUTES } from '@/constants/routerConstants'; describe('로그인 통합 테스트', () => { beforeEach(() => { diff --git a/cypress/e2e/System/AboutPage/Introduction.cy.ts b/cypress/e2e/System/AboutPage/Introduction.cy.ts index 7d5a932..bebbf83 100644 --- a/cypress/e2e/System/AboutPage/Introduction.cy.ts +++ b/cypress/e2e/System/AboutPage/Introduction.cy.ts @@ -1,40 +1,13 @@ -import { aboutPageAttributes, dataEditCompanyPageAttributes } from '@/constants/dataCyAttributes'; -import { MSG } from '@/constants/messages'; -import { confirmAndCheckCompletion, login } from 'cypress/support/hooks'; +import { login, normalizeHtml } from 'cypress/support/hooks'; describe('1. 회사 소개를 관리한다', () => { beforeEach(() => { login(); - cy.intercept('GET', '/api/company/information', { - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '전체 회사 정보를 성공적으로 조회하였습니다.', - data: { - id: 1, - mainOverview: '

STUDIO EYE IS THE BEST NEW MEDIA

PRODUCTION BASED ON OTT & YOUTUBE

', - commitment: '

우리는 급변하는 뉴 미디어 시대를 반영한 콘텐츠 제작을 위해 끊임없이 고민하고 변화합니다.

', - address: '서울시 성동구 광나루로 162 BS성수타워 5층', - addressEnglish: '5F 162, Gwangnaru-ro, Seongdong-gu, Seoul, Republic of Korea', - phone: '02-2038-2663', - fax: '02-2038-2663', - introduction: '초기 테스트 입력', - sloganImageFileName: null, - sloganImageUrl: null, - detailInformation: [ - { - id: 1, - key: '문의하기', - value: '사이트를 통해 간편하게 문의하세요.', - }, - ], - }, - }, - }).as('getCompanyInfo'); + cy.intercept('GET', '/api/company/information').as('getCompanyInfo'); + cy.visit('/promotion-admin/dataEdit/company'); + cy.wait('@getCompanyInfo', { timeout: 10000 }); cy.visit('/promotion-admin/dataEdit/company'); // 회사 정보 편집 페이지로 이동 - cy.wait('@getCompanyInfo', { timeout: 10000 }); }); it('PP에서 로그인 후 PA의 회사 정보 편집 페이지로 이동한다', () => { @@ -42,225 +15,99 @@ describe('1. 회사 소개를 관리한다', () => { cy.url().should('include', '/promotion-admin/dataEdit/company'); }); - // it('초기 회사 소개 텍스트를 생성하고 확인한다', () => { - // cy.fixture('AboutPage/introductionData.json').then((data) => { - // // 기본 데이터 입력 - // cy.get(`[data-cy="${aboutPageAttributes.CREATE_INTRO}"]`).type(data.initialIntroduction); - // cy.get(`[data-cy="${dataEditCompanyPageAttributes.SUBMIT_BUTTON}"]`).click(); - - // // 등록 확인 프롬프트 및 완료 메시지 확인 - // confirmAndCheckCompletion(MSG.CONFIRM_MSG.POST, MSG.ALERT_MSG.POST); // 메시지 확인 - - // cy.visit('/about'); - // cy.contains(data.initialIntroduction).should('exist'); - // }); - // }); - it('PA에서 회사 소개 텍스트를 수정하고 PP에서 변경된 내용을 확인한다', () => { - cy.fixture('AboutPage/introductionData.json').then((data) => { - // 수정 API 모킹 - cy.intercept('PUT', '/api/company/introduction', (req) => { - const expectedData = { - mainOverview: '

STUDIO EYE IS THE BEST NEW MEDIA

PRODUCTION BASED ON OTT & YOUTUBE

', - commitment: '

우리는 급변하는 뉴 미디어 시대를 반영한 콘텐츠 제작을 위해 끊임없이 고민하고 변화합니다.

', - introduction: data.updatedIntroduction, - }; - - // HTML 태그와 줄바꿈을 제거하고 비교 - const stripHtml = (str: string) => str.replace(/<[^>]*>/g, '').trim(); - const normalizeText = (str: string) => str.replace(/\n/g, '').trim(); - chai - .expect(normalizeText(stripHtml(req.body.introduction))) - .to.include(normalizeText(stripHtml(expectedData.introduction))); - - req.reply({ - statusCode: 200, - body: { - message: 'Company Introduction updated successfully', - }, - }); - }).as('updateIntroduction'); - - // GET 요청에 대한 모킹 추가: 페이지 이동 후 확인을 위해 - cy.intercept('GET', '/api/company/information', { - statusCode: 200, - body: { - code: 200, - status: 'OK', - data: { - id: 1, - mainOverview: '

STUDIO EYE IS THE BEST NEW MEDIA

PRODUCTION BASED ON OTT & YOUTUBE

', - commitment: - '

우리는 급변하는 뉴 미디어 시대를 반영한 콘텐츠 제작을 위해 끊임없이 고민하고 변화합니다.

', - address: '서울시 성동구 광나루로 162 BS성수타워 5층', - addressEnglish: '5F 162, Gwangnaru-ro, Seongdong-gu, Seoul, Republic of Korea', - phone: '02-2038-2663', - fax: '02-2038-2663', - introduction: data.updatedIntroduction, - sloganImageFileName: null, - sloganImageUrl: null, - detailInformation: [ - { - id: 1, - key: '문의하기', - value: '사이트를 통해 간편하게 문의하세요.', - }, - ], - }, - }, - }).as('getUpdatedCompanyInfo'); - - // MODIFY_INTRO_TITLE 요소가 존재하고 보이는지 확인 - cy.get('[data-cy="MODIFY_INTRO_TITLE"]', { timeout: 20000 }).should('exist').and('be.visible'); - - cy.log('MODIFY_INTRO_TITLE 요소 확인 완료'); - - // 수정하기 버튼 클릭 - cy.get(`[data-cy="dataEdit-Button"]`, { timeout: 20000 }) - .should('exist') - .and('be.visible') - .eq(2) // 세 번째 요소 선택 - .click({ force: true }); - - cy.log('수정하기 버튼 클릭 완료'); - - // 에디터가 존재하는지 확인 후 텍스트 입력 - cy.wait(1000); - cy.get('.ql-editor', { timeout: 20000 }) - .should('exist') - .and('be.visible') - .eq(2) // 세 번째 요소만 선택 - .invoke('html', ''); // 기존 내용 삭제 + // 기존 데이터 확인 + cy.wait('@getCompanyInfo').then((interception) => { + const response = interception.response?.body; + chai.expect(response).to.have.property('code', 200); + chai.expect(response.data).to.have.property('introduction'); + }); - cy.get('.ql-editor').eq(2).type(data.updatedIntroduction, { force: true }); + // 수정하기 버튼 클릭 + cy.get(`[data-cy="dataEdit-Button"]`).eq(2).click({ force: true }); - cy.log('텍스트 입력 완료'); + // 텍스트 입력 + cy.get('.ql-editor') + .eq(2) + .invoke('html', '') // 기존 내용 제거 + .type('

수정된 회사 소개 텍스트

', { parseSpecialCharSequences: false, force: true }); - // "저장하기"라는 텍스트를 포함한 버튼 클릭 - cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').and('be.visible').click({ force: true }); + // 요청 가로채기 및 확인 + cy.intercept('PUT', '/api/company/introduction').as('updateIntroduction'); + cy.contains('button', '저장하기').click({ force: true }); - // 모킹된 API 요청 대기 - cy.wait('@updateIntroduction'); + cy.wait('@updateIntroduction', { timeout: 10000 }).then((interception) => { + const request = interception.request?.body; - cy.log('저장하기 버튼 클릭 완료 및 API 호출 완료'); + // HTML 태그 제거 후 순수 텍스트 비교 + chai.expect(normalizeHtml(request.introduction)).to.equal('수정된 회사 소개 텍스트'); - // 수정된 내용 확인을 위한 GET 요청 대기 - cy.wait('@getUpdatedCompanyInfo'); + const response = interception.response?.body; + chai.expect(response.message).to.equal('회사 소개 정보를 성공적으로 수정했습니다.'); + }); - // 수정된 내용 확인 - cy.visit('/about'); + // 수정된 데이터 확인 + cy.intercept('GET', '/api/company/information').as('getUpdatedCompanyInfo'); + cy.wait('@getUpdatedCompanyInfo').then((interception) => { + const response = interception.response?.body; - // 수정된 내용 확인을 위해 페이지를 천천히 스크롤하며 요소 렌더링 확인 - cy.visit('/about'); - for (let i = 0; i < 8; i++) { - cy.scrollTo(0, i * 300, { duration: 500 }); - cy.wait(500); // 스크롤 후 대기 시간 추가 - } - cy.get('[data-cy="about-content"]').should('contain', data.updatedIntroduction); + // HTML 태그 제거 후 순수 텍스트 비교 + chai.expect(normalizeHtml(response.data.introduction)).to.equal('수정된 회사 소개 텍스트'); }); + + cy.visit('/about'); + cy.get('[data-cy="about-content"]').should('contain', '수정된 회사 소개 텍스트'); }); it('PA에서 회사 소개 텍스트를 삭제하고 PP에서 기본 데이터를 확인한다', () => { - cy.fixture('AboutPage/introductionData.json').then((data) => { - // API 요청 모킹 설정 - cy.intercept('PUT', '/api/company/introduction', (req) => { - const expectedData = { - mainOverview: '

STUDIO EYE IS THE BEST NEW MEDIA

PRODUCTION BASED ON OTT & YOUTUBE

', - commitment: '

우리는 급변하는 뉴 미디어 시대를 반영한 콘텐츠 제작을 위해 끊임없이 고민하고 변화합니다.

', - introduction: '', // 삭제된 상태 - }; - - // HTML 태그와 줄바꿈을 제거하고 비교 - const stripHtml = (str: string) => str.replace(/<[^>]*>/g, '').trim(); - const normalizeText = (str: string) => str.replace(/\n/g, '').trim(); - chai - .expect(normalizeText(stripHtml(req.body.introduction))) - .to.include(normalizeText(stripHtml(expectedData.introduction))); + cy.fixture('AboutPage/introductionData.json').then(() => { + // 기존 데이터 가져오기 확인 + cy.wait('@getCompanyInfo').then((interception) => { + const response = interception.response?.body; + chai.expect(response).to.have.property('code', 200); + chai.expect(response.data).to.have.property('introduction'); + }); - req.reply({ - statusCode: 200, - body: {}, - }); - }).as('deleteIntroduction'); + // 수정하기 버튼 클릭 + cy.get(`[data-cy="dataEdit-Button"]`).eq(2).click({ force: true }); - // GET 요청에 대한 모킹 추가: 페이지 이동 후 확인을 위해 - cy.intercept('GET', '/api/company/information', { - statusCode: 200, - body: { - code: 200, - status: 'OK', - data: { - id: 1, - mainOverview: '

STUDIO EYE IS THE BEST NEW MEDIA

PRODUCTION BASED ON OTT & YOUTUBE

', - commitment: - '

우리는 급변하는 뉴 미디어 시대를 반영한 콘텐츠 제작을 위해 끊임없이 고민하고 변화합니다.

', - address: '서울시 성동구 광나루로 162 BS성수타워 5층', - addressEnglish: '5F 162, Gwangnaru-ro, Seongdong-gu, Seoul, Republic of Korea', - phone: '02-2038-2663', - fax: '02-2038-2663', - introduction: - '

2010년에 설립된 스튜디오 아이는 다양한 장르를 소화할 수 있는 PD들이 모여

클라이언트 맞춤형 콘텐츠 제작 운영 대책 서비스를 제공하고 있으며

드라마 애니메이션 등을 전문으로 하는 여러 계열사들과도 협력하고 있습니다.

', - sloganImageFileName: null, - sloganImageUrl: null, - detailInformation: [ - { - id: 1, - key: '문의하기', - value: '사이트를 통해 간편하게 문의하세요.', - }, - ], - }, - }, - }).as('getUpdatedCompanyInfo'); + // 에디터에 빈값 입력 + cy.get('.ql-editor') + .eq(2) + .invoke('html', '') // 기존 내용 삭제 + .type('{selectall}{backspace}', { force: true }); // 모든 내용 지우기 - // MODIFY_INTRO_TITLE 요소가 존재하고 보이는지 확인 - cy.get('[data-cy="MODIFY_INTRO_TITLE"]', { timeout: 20000 }).should('exist').and('be.visible'); + // 저장하기 버튼 클릭 + cy.intercept('PUT', '/api/company/introduction').as('updateIntroduction'); + cy.contains('button', '저장하기').click({ force: true }); - cy.log('MODIFY_INTRO_TITLE 요소 확인 완료'); // 디버깅용 로그 + // 삭제 요청 확인 + cy.wait('@updateIntroduction', { timeout: 10000 }).then((interception) => { + const request = interception.request?.body; - // 수정하기 버튼 클릭 - cy.get(`[data-cy="dataEdit-Button"]`, { timeout: 20000 }) - .should('exist') - .and('be.visible') - .eq(2) // 세 번째 요소 선택 - .click({ force: true }); + // "


"를 빈값으로 간주 + const normalizeIntroduction = (introduction: string) => (introduction === '


' ? '' : introduction); - cy.log('수정하기 버튼 클릭 완료'); // 디버깅용 로그 + chai.expect(normalizeIntroduction(request.introduction)).to.equal(''); // 빈값 확인 - // 에디터가 존재하는지 확인 후 텍스트 삭제 - cy.wait(1000); // 요소 로딩 대기 - cy.get('.ql-editor', { timeout: 20000 }) // 클래스명을 사용해 요소 찾기 - .should('exist') - .and('be.visible') - .eq(2) // 세 번째 요소만 선택 - .invoke('html', ''); // 내용 삭제 + const response = interception.response?.body; + chai.expect(response.message).to.equal('회사 소개 정보를 성공적으로 수정했습니다.'); + }); - // "저장하기"라는 텍스트를 포함한 버튼 클릭 - cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').and('be.visible').click({ force: true }); + // 기본 데이터 확인 + cy.intercept('GET', '/api/company/information').as('getUpdatedCompanyInfo'); + cy.wait('@getUpdatedCompanyInfo').then((interception) => { + const response = interception.response?.body; - // 모킹된 API 요청 대기 - cy.wait('@deleteIntroduction'); + // "


"를 빈값으로 간주하여 확인 + const normalizeIntroduction = (introduction: string) => (introduction === '


' ? '' : introduction); - // 기본 정보 확인을 위해 GET 요청 대기 - cy.wait('@getUpdatedCompanyInfo'); + chai.expect(normalizeIntroduction(response.data.introduction)).to.equal(''); + }); - // 페이지를 천천히 스크롤하며 요소 렌더링 확인 + // /about 페이지 이동 후 기본 값 확인 cy.visit('/about'); - for (let i = 0; i < 8; i++) { - cy.scrollTo(0, i * 100, { duration: 500 }); // 100픽셀씩 천천히 스크롤 - cy.wait(500); // 스크롤 후 대기 시간 추가 - } - cy.get('[data-cy="about-content"]') - .invoke('text') - .then((text) => { - const expectedText = - '2010년에 설립된 스튜디오 아이는 다양한 장르를 소화할 수 있는 PD들이 모여클라이언트 맞춤형 콘텐츠 제작과 운영 대책 서비스를 제공하고 있으며드라마 애니메이션 등을 전문으로 하는 여러 계열사들과도 협력하고 있습니다.'; - - // 모든 공백을 단일 공백으로 정규화하고 문자열 앞뒤 공백 제거 - const normalizeText = (str: string) => str.replace(/\s+/g, ' ').trim(); - - chai.expect(normalizeText(text)).to.include(normalizeText(expectedText)); - }); + cy.get('[data-cy="about-content"]').should('not.contain', '수정된 회사 소개 텍스트'); // 수정된 텍스트가 없는지 확인 }); }); }); diff --git a/cypress/e2e/System/AboutPage/Slogan.cy.ts b/cypress/e2e/System/AboutPage/Slogan.cy.ts index ee6e60a..2338c62 100644 --- a/cypress/e2e/System/AboutPage/Slogan.cy.ts +++ b/cypress/e2e/System/AboutPage/Slogan.cy.ts @@ -3,37 +3,15 @@ import { login } from 'cypress/support/hooks'; describe('2. 회사 슬로건을 관리한다.', () => { beforeEach(() => { login(); - // 회사 정보 모킹 - cy.intercept('GET', '/api/company/information', { - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '전체 회사 정보를 성공적으로 조회하였습니다.', - data: { - id: 1, - mainOverview: '

STUDIO EYE IS THE BEST NEW MEDIA

PRODUCTION BASED ON OTT & YOUTUBE

', - commitment: '

우리는 급변하는 뉴 미디어 시대를 반영한 콘텐츠 제작을 위해 끊임없이 고민하고 변화합니다.

', - address: '서울시 성동구 광나루로 162 BS성수타워 5층', - addressEnglish: '5F 162, Gwangnaru-ro, Seongdong-gu, Seoul, Republic of Korea', - phone: '02-2038-2663', - fax: '02-2038-2663', - introduction: '초기 테스트 입력', - sloganImageFileName: null, - sloganImageUrl: 'https://studio-eye-gold-bucket.s3.ap-northeast-2.amazonaws.com/Slogan.png', - detailInformation: [ - { - id: 1, - key: '문의하기', - value: '사이트를 통해 간편하게 문의하세요.', - }, - ], - }, - }, - }).as('getCompanyInfo'); + cy.intercept('GET', '/api/company/information').as('getCompanyInfo'); cy.visit('/promotion-admin/dataEdit/company'); // 회사 정보 편집 페이지로 이동 - cy.wait('@getCompanyInfo', { timeout: 10000 }); + cy.wait('@getCompanyInfo', { timeout: 10000 }).then((interception) => { + const response = interception.response?.body; + chai.expect(response).to.have.property('code', 200); + chai.expect(response.data).to.have.property('sloganImageUrl'); + }); + cy.intercept('PUT', '/api/company/slogan').as('updateSlogan'); }); it('PP에서 로그인 후 PA의 회사 정보 편집 페이지로 이동한다', () => { @@ -42,6 +20,9 @@ describe('2. 회사 슬로건을 관리한다.', () => { }); it('PA에서 회사 슬로건 이미지를 수정하고 PP에서 변경된 이미지를 확인한다', () => { + // PUT 요청 인터셉트 + cy.intercept('PUT', '/api/company/slogan').as('updateSlogan'); + // 두 번째 수정하기 버튼 클릭 cy.get(`[data-cy="dataEdit-Button"]`).eq(1).should('exist').and('be.visible').click({ force: true }); @@ -51,14 +32,13 @@ describe('2. 회사 슬로건을 관리한다.', () => { // 저장하기 버튼 클릭 cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').and('be.visible').click({ force: true }); + // 저장 확인 알림 처리 + cy.on('window:confirm', (text) => { + return true; + }); + // /about 페이지 방문 후 변경된 슬로건 이미지 확인 cy.visit('/about'); - cy.wait(2000); // 이미지 반영을 기다리기 위한 추가 대기 시간 - - cy.get('img').then(($img) => { - const imgSrc = $img.attr('src'); - cy.log('Image src:', imgSrc); // 로그 출력 - chai.expect(imgSrc).to.contain('studioeyeyellow'); - }); + cy.get('[data-cy="mission-image"]').should('have.attr', 'src').and('contain', 'Slogan.png'); // 이미지 파일 이름에 "Slogan.png" 포함 확인 }); }); diff --git a/cypress/e2e/System/AboutPage/WhatWeDo.cy.ts b/cypress/e2e/System/AboutPage/WhatWeDo.cy.ts index 73619c3..3c90575 100644 --- a/cypress/e2e/System/AboutPage/WhatWeDo.cy.ts +++ b/cypress/e2e/System/AboutPage/WhatWeDo.cy.ts @@ -4,40 +4,8 @@ import { login } from 'cypress/support/hooks'; describe('3. WHAT WE DO를 관리한다', () => { beforeEach(() => { login(); - - cy.intercept('GET', `/api/company/information`, { - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '전체 회사 정보를 성공적으로 조회하였습니다.', - data: { - id: 9999, - mainOverview: '

스튜디오 아이와 함께 영상물 퀄리티 UP 

', - commitment: '

최고의 경험을 선사하는 스튜디오 아이의 작업과 함께하세요.

', - address: '서울시 성동구 광나루로 162 BS성수타워 5층', - phone: '02-2038-2663', - detailInformation: [ - { - id: 5555, - key: 'MCN 2.0', - value: '뉴미디어 콘텐츠에 체화된 플래디만의 독보적인 제작 역량을 바탕으로 크리에이터와 함께 성장합니다', - }, - { - id: 6666, - key: 'Digital Operator', - value: '다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - }, - { - id: 7777, - key: 'PD Group', - value: '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - }, - ], - }, - }, - }).as('getCompanyInfo'); - + cy.intercept('GET', `/api/company/information`).as('getCompanyInfo'); + cy.intercept('GET', '/api/company/detail').as('getDetailForEdit'); cy.visit('/promotion-admin/dataEdit/company'); cy.wait('@getCompanyInfo'); // 초기 데이터가 로드될 때까지 대기 }); @@ -47,65 +15,19 @@ describe('3. WHAT WE DO를 관리한다', () => { }); it('PA에서 기존에 존재하는 WHAT WE DO를 수정하고 PP에서 변경된 내용을 확인한다', () => { - // 수정하기 버튼 클릭 cy.get(`[data-cy="dataEdit-Button"]`, { timeout: 20000 }) .should('exist') .and('be.visible') .eq(3) .click({ force: true }); - // 수정 페이지에서 GET 요청 모킹 - cy.intercept('GET', '/api/company/detail', { - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '회사 상세 정보를 성공적으로 조회하였습니다.', - data: [ - { - id: 5555, - key: 'MCN 2.0', - value: '뉴미디어 콘텐츠에 체화된 플래디만의 독보적인 제작 역량을 바탕으로 크리에이터와 함께 성장합니다', - }, - { - id: 6666, - key: 'Digital Operator', - value: '다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - }, - { - id: 7777, - key: 'PD Group', - value: '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - }, - ], - }, - }).as('getDetailForEdit'); - - // PUT 요청 모킹 - 세부 정보를 업데이트하는 API - cy.intercept('PUT', '/api/company/detail', (req) => { - const { detailInformation } = req.body; - detailInformation.forEach((detail: { key: string; value: string }, index: number) => { - // 공백과 개행 문자를 제거한 후 비교 - chai.expect(detail.key.trim()).to.equal(['MCN 2.0', 'Digital Operator', 'PD Group'][index].trim()); - chai - .expect(detail.value.replace(/\s+/g, ' ').trim()) - .to.equal( - [ - '정말로 다양하고 멋진 작업물을 문의해보세요', - '정말로 다양하고 멋진 다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - ][index] - .replace(/\s+/g, ' ') - .trim(), - ); - }); - req.reply({ - statusCode: 200, - body: {}, - }); - }).as('updateCompanyInfo'); + // 수정하기 버튼 클릭 후 요청 대기 + cy.wait('@getDetailForEdit', { timeout: 10000 }).then((interception) => { + const response = interception.response?.body; + chai.expect(response).to.have.property('code', 200); + chai.expect(response.data).to.be.an('array'); + }); - // 기존 Detail 수정 const updatedDetails = [ { key: 'MCN 2.0', @@ -120,188 +42,92 @@ describe('3. WHAT WE DO를 관리한다', () => { updatedDetails.forEach((detail, index) => { cy.get(`[data-cy="${aboutPageAttributes.DETAIL_KEY_INPUT}"]`) .eq(index) - .should('exist') // 존재 여부 확인 - .scrollIntoView() // 요소를 보이도록 스크롤 - .should('be.visible') // 보이는지 확인 - .clear({ force: true }) // 강제로 클리어 - .type(detail.key) // 새로운 키 입력 - .should('have.value', detail.key); // 입력된 값 확인 - - cy.wait(500); + .clear({ force: true }) + .type(detail.key, { force: true }); cy.get(`[data-cy="${aboutPageAttributes.DETAIL_VALUE_INPUT}"]`) .eq(index) - .should('exist') // 존재 여부 확인 - .scrollIntoView() // 요소를 보이도록 스크롤 - .should('be.visible') // 보이는지 확인 - .clear({ force: true }) // 강제로 클리어 - .type(detail.value) // 새로운 값 입력 - .should('have.value', detail.value); // 입력된 값 확인 + .clear({ force: true }) + .type(detail.value, { force: true }); }); - // "저장하기" 버튼 클릭 - cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').and('be.visible').click({ force: true }); - cy.wait('@updateCompanyInfo'); // 업데이트 요청 대기 + // 저장하기 버튼 클릭 후 PUT 요청 가로채기 + cy.intercept('PUT', '/api/company/detail').as('updateCompanyInfo'); + cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').click({ force: true }); - cy.visit('/about'); + cy.wait('@updateCompanyInfo', { timeout: 10000 }).then((interception) => { + const request = interception.request?.body; - // /about 페이지에서 수정된 항목 확인 - cy.intercept('GET', `/api/company/detail`, { - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '회사 상세 정보를 성공적으로 조회하였습니다.', - data: [ - { - id: 5555, - key: 'MCN 2.0', - value: '정말로 다양하고 멋진 작업물을 문의해보세요', - }, - { - id: 6666, - key: 'Digital Operator', - value: '정말로 다양하고 멋진 다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - }, - { - id: 7777, - key: 'PD Group', - value: '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - }, - ], - }, - }).as('getUpdatedCompanyInfo'); + // 요청 데이터 검증 + const requestIncludesDetail = (detail: { key: any; value: any }) => + request.detailInformation.some( + (info: { key: any; value: any }) => info.key === detail.key && info.value === detail.value, + ); + updatedDetails.forEach((detail) => chai.expect(requestIncludesDetail(detail)).to.be.true); - cy.wait('@getUpdatedCompanyInfo'); + const response = interception.response?.body; + chai.expect(response).to.have.property('status', 'OK'); + chai.expect(response.message).to.equal('회사 5가지 상세 정보를 성공적으로 수정했습니다.'); + }); - // 변경된 내용 확인 - updatedDetails.forEach((detail) => { - cy.contains(detail.value, { timeout: 10000 }).should('exist'); // 업데이트된 내용 확인 + // /about 페이지로 이동하여 응답 데이터 확인 + cy.visit('/about'); + cy.intercept('GET', `/api/company/detail`).as('getUpdatedCompanyDetail'); // 경로 수정 + cy.wait('@getUpdatedCompanyDetail', { timeout: 10000 }).then((interception) => { + const response = interception.response?.body; + + // 응답 데이터 검증 + const responseIncludesDetail = (detail: { key: any; value: any }) => + response.data.some((info: { key: any; value: any }) => info.key === detail.key && info.value === detail.value); + updatedDetails.forEach((detail) => chai.expect(responseIncludesDetail(detail)).to.be.true); }); }); it('PA에서 기존에 존재하는 WHAT WE DO 항목을 삭제하고 PP에서 변경된 내용을 확인한다', () => { - // 수정하기 버튼 클릭 cy.get(`[data-cy="dataEdit-Button"]`, { timeout: 20000 }) .should('exist') .and('be.visible') .eq(3) .click({ force: true }); - // PUT 요청 모킹 - 세부 정보를 업데이트하는 API - cy.intercept('PUT', '/api/company/detail', (req) => { - const { detailInformation } = req.body; - detailInformation.forEach((detail: { key: string; value: string }, index: number) => { - chai.expect(detail.key.trim()).to.equal(['Digital Operator', 'PD Group'][index].trim()); - chai - .expect(detail.value.replace(/\s+/g, ' ').trim()) - .to.equal( - [ - '다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - ][index] - .replace(/\s+/g, ' ') - .trim(), - ); - }); - req.reply({ - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '세부 정보가 성공적으로 업데이트되었습니다.', - data: [ - { - id: 6666, - key: 'Digital Operator', - value: '다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - }, - { - id: 7777, - key: 'PD Group', - value: '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - }, - ], - }, - }); - }).as('deleteCompanyInfo'); - - // 첫 번째 항목 삭제 - cy.get(`[data-cy="${aboutPageAttributes.DELETE_DETAIL}"]`).eq(0).click({ force: true }); // 첫 번째 삭제 버튼 클릭 - - // 프롬프트 창 자동 확인 처리 + cy.get(`[data-cy="${aboutPageAttributes.DELETE_DETAIL}"]`).first().click({ force: true }); cy.on('window:confirm', () => true); - cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').and('be.visible').click({ force: true }); - - // 업데이트 요청 대기 - cy.wait('@deleteCompanyInfo'); + cy.intercept('PUT', '/api/company/detail').as('deleteCompanyInfo'); - // 로그는 체이닝과 분리하여 별도로 실행 - cy.log('저장하기 버튼 클릭 완료'); + cy.contains('button', '저장하기', { timeout: 20000 }).should('exist').click({ force: true }); - // /about 페이지에서 수정된 항목 확인 - cy.intercept('GET', `/api/company/information`, { - statusCode: 200, - body: { - code: 200, - status: 'OK', - message: '전체 회사 정보를 성공적으로 조회하였습니다.', - data: { - id: 9999, - mainOverview: '

스튜디오 아이와 함께 영상물 퀄리티 UP 

', - commitment: '

최고의 경험을 선사하는 스튜디오 아이의 작업과 함께하세요.

', - address: '서울시 성동구 광나루로 162 BS성수타워 5층', - addressEnglish: '5F 162, Gwangnaru-ro, Seongdong-gu, Seoul, Republic of Korea', - phone: '02-2038-2663', - fax: '02-2038-2663', - introduction: '

스튜디오 아이는 편집 및 애니메이팅을 하고 있는 영상 매체 작업 전문 기업입니다.

', - sloganImageFileName: 'Slogan.png', - sloganImageUrl: 'https://studio-eye-gold-bucket.s3.ap-northeast-2.amazonaws.com/Slogan.png', - detailInformation: [ - { - id: 6666, - key: 'Digital Operator', - value: '다양한 고객의 Youtube, Instagram, TikTok 채널을 운영 대행합니다', - }, - { - id: 7777, - key: 'PD Group', - value: '예능, 드라마, 다큐멘터리, 애니메이션까지 전 장르의 디지털 콘텐츠를 기획, 제작합니다', - }, - ], - }, - }, - }).as('getDeletedCompanyInfo'); + cy.wait('@deleteCompanyInfo').then((interception) => { + const request = interception.request?.body; + chai.expect(request.detailInformation).to.have.length.above(0); // 최소 1개 남아있음 + }); cy.visit('/about'); - cy.wait('@getDeletedCompanyInfo'); + cy.intercept('GET', `/api/company/information`).as('getDeletedCompanyInfo'); + + cy.wait('@getDeletedCompanyInfo').then((interception) => { + const response = interception.response?.body; + chai.expect(response.data.detailInformation).to.not.deep.include({ + key: 'MCN 2.0', + }); + }); cy.contains('MCN 2.0').should('not.exist'); }); - it('PA에서 WHAT WE DO 항목을 삭제할 때 최소 하나 이상 남아있어야 한다는 알림을 확인한다.', () => { - // 수정하기 버튼 클릭 + it('필수 예외) PA에서 WHAT WE DO 항목을 삭제할 때 최소 하나 이상 남아있어야 한다는 알림을 확인한다.', () => { cy.get(`[data-cy="dataEdit-Button"]`, { timeout: 20000 }) .should('exist') .and('be.visible') .eq(3) .click({ force: true }); - cy.get(`[data-cy="${aboutPageAttributes.DELETE_DETAIL}"]`, { timeout: 10000 }).should('exist'); + cy.get(`[data-cy="${aboutPageAttributes.DELETE_DETAIL}"]`).then(($buttons) => { if ($buttons.length === 1) { - // 항목이 하나만 남아있을 때 삭제 버튼 클릭 cy.wrap($buttons.eq(0)).click({ force: true }); - - // 프롬프트 창 자동 확인 처리 cy.on('window:confirm', () => true); - // 최소 1개 이상 등록되어 있어야 한다는 알림이 표시되는지 확인 cy.contains('최소 1개 이상은 등록되어 있어야 합니다.').should('be.visible'); - } else { - // 여러 개가 남아있을 때는 일반 삭제 시나리오 실행 - cy.wrap($buttons.eq(0)).click({ force: true }); - cy.on('window:confirm', () => true); } }); }); diff --git a/cypress/support/hooks.ts b/cypress/support/hooks.ts index 6ded828..95b04fb 100644 --- a/cypress/support/hooks.ts +++ b/cypress/support/hooks.ts @@ -23,3 +23,17 @@ export const confirmAndCheckCompletion = (confirmMessage: string, alertMessage: return true; // 알림 확인 }); }; + +// HTML 태그 제거 및 HTML 엔티티 디코딩 함수 +export const normalizeHtml = (html: string) => { + // HTML 엔티티 디코딩 + const textarea = document.createElement('textarea'); + textarea.innerHTML = html; + const decodedHtml = textarea.value; + + // 태그 제거 및 텍스트 정리 + return decodedHtml + .replace(/<[^>]*>/g, '') // HTML 태그 제거 + .replace(/\s+/g, ' ') // 공백 정리 + .trim(); // 앞뒤 공백 제거 +}; diff --git a/src/components/PromotionAdmin/Home/Graph/Graph.tsx b/src/components/PromotionAdmin/Home/Graph/Graph.tsx index 11b7faa..8412d1f 100644 --- a/src/components/PromotionAdmin/Home/Graph/Graph.tsx +++ b/src/components/PromotionAdmin/Home/Graph/Graph.tsx @@ -11,8 +11,8 @@ type Props = { title: string; processedData: { x: string; y: number }[]; data: ViewData[] | RequestData[]; - handleCategoryChange: (category:string)=>void; - handleStateChange: (state:string)=>void; + handleCategoryChange: (category: string) => void; + handleStateChange: (state: string) => void; handleStartDateChange: (newStartDate: dayjs.Dayjs | null) => void; handleEndDateChange: (newEndDate: dayjs.Dayjs | null) => void; category: string; @@ -24,10 +24,10 @@ type Props = { filter2: Option[]; }; -type Option={ - value:string; - label:string; -} +type Option = { + value: string; + label: string; +}; const Graph = ({ title, @@ -45,14 +45,14 @@ const Graph = ({ filter, filter2, }: Props) => { - const [showFilter2, setShowFilter2] = useState(false); - useEffect(()=>{ - if(division==='request'||category===MenuType.ARTWORK){ + const [showFilter2, setShowFilter2] = useState(false); + useEffect(() => { + if (division === 'request' || category === MenuType.ARTWORK) { setShowFilter2(true); - }else{ + } else { setShowFilter2(false); } - },[category]) + }, [category]); return ( @@ -70,17 +70,40 @@ const Graph = ({ /> -
- handleCategoryChange(e.target.value)}> - {filter&&filter.map((option)=>{ - return {option.label} - })} +
+ handleCategoryChange(e.target.value)} + > + {filter && + filter.map((option, index) => { + return ( + + {option.label} + + ); + })} - handleStateChange(e.target.value)} disabled={showFilter2?false:true}> - {filter2&&filter2.map((option,index)=>{ - return {option.label} - })} + handleStateChange(e.target.value)} + disabled={showFilter2 ? false : true} + > + {filter2 && + filter2.map((option, index) => { + return ( + + {option.label} + + ); + })}
@@ -159,7 +182,7 @@ const ErrorWrapper = styled.div` } `; -const FilterSelect=styled.select` +const FilterSelect = styled.select` min-width: fit-content; height: fit-content; backdrop-filter: blur(4px); @@ -168,8 +191,8 @@ const FilterSelect=styled.select` padding: 4px; font-size: 0.9rem; font-family: 'pretendard'; -` -const FilterOption=styled.option` -font-size: 0.9rem; -font-family: pretendard; -` \ No newline at end of file +`; +const FilterOption = styled.option` + font-size: 0.9rem; + font-family: pretendard; +`; diff --git a/src/components/PromotionAdmin/Home/Graph/LineGraph.tsx b/src/components/PromotionAdmin/Home/Graph/LineGraph.tsx index 7846db5..646a7a2 100644 --- a/src/components/PromotionAdmin/Home/Graph/LineGraph.tsx +++ b/src/components/PromotionAdmin/Home/Graph/LineGraph.tsx @@ -9,10 +9,10 @@ type LineGraphProps = { const LineGraph = ({ data, division }: LineGraphProps) => { const colors = division === 'request' ? ['#0064FF'] : ['#E16262']; - const yValues = data.map(d => d.y); + const yValues = data.map((d) => d.y); const maxY = Math.max(...yValues); // 최대값 계산 const numberOfTicks = 5; // 원하는 tick 개수 - let tickValues:number[]=[]; + let tickValues: number[] = []; // yValues가 비어 있지 않은 경우에만 tickValues를 계산 if (maxY > 0) { const interval = maxY / (numberOfTicks - 1); // 간격 계산 @@ -47,14 +47,6 @@ const LineGraph = ({ data, division }: LineGraphProps) => { legend: '', legendOffset: 36, legendPosition: 'middle', - truncateTickAt: 0, - format: (value) => { - const parts = value.split(' '); - if (parts.length === 2) { - return parts[1]; - } - return value; - }, }} axisLeft={{ tickSize: 5, @@ -63,40 +55,71 @@ const LineGraph = ({ data, division }: LineGraphProps) => { legend: '', legendOffset: -40, legendPosition: 'middle', - truncateTickAt: 0, tickValues: tickValues, - format: (value) => Math.round(value).toString(), }} + enableGridX={true} + enableGridY={true} + animate={true} enablePoints={true} pointSize={10} pointColor={{ theme: 'background' }} pointBorderWidth={2} pointBorderColor={{ from: 'serieColor' }} + pointLabel={(d) => `${d.y}`} pointLabelYOffset={-12} + enablePointLabel={false} // 포인트 레이블 비활성화 enableArea={true} - enableTouchCrosshair={true} + areaOpacity={0.3} + areaBaselineValue={0} + areaBlendMode='normal' + lineWidth={2} + legends={[]} // 범례 비활성화 + isInteractive={true} + debugMesh={false} + enableSlices='x' // 슬라이스 활성화 + debugSlices={false} + enableCrosshair={true} + crosshairType='bottom-left' + role='application' + defs={[]} // 그래프 패턴 정의 + fill={[]} // 채우기 스타일 정의 useMesh={true} - tooltip={(tooltip) => { - return ( -
-
- {tooltip.point.data.xFormatted} -
-
- {division === 'request' ? '문의 수' : '조회 수'} {Math.round(Number(tooltip.point.data.y))} + layers={['grid', 'markers', 'axes', 'areas', 'lines', 'points', 'slices', 'mesh', 'legends']} // 필수 추가 + sliceTooltip={({ slice }) => ( +
+ {slice.points.map((point) => ( +
+ {point.data.xFormatted}: {point.data.yFormatted}
+ ))} +
+ )} + tooltip={(tooltip) => ( +
+
+ {tooltip.point.data.xFormatted}
- ); - }} +
+ {division === 'request' ? '문의 수' : '조회 수'} {Math.round(Number(tooltip.point.data.y))} +
+
+ )} />
); diff --git a/src/components/PromotionAdmin/Home/RequestSummary/RequestSummary.tsx b/src/components/PromotionAdmin/Home/RequestSummary/RequestSummary.tsx index 4bf519c..3dc38df 100644 --- a/src/components/PromotionAdmin/Home/RequestSummary/RequestSummary.tsx +++ b/src/components/PromotionAdmin/Home/RequestSummary/RequestSummary.tsx @@ -22,7 +22,7 @@ const WatingRequests = () => { const handleSort = () => { setSortByRecent((prev) => !prev); }; - console.log(data); + return ( @@ -34,7 +34,7 @@ const WatingRequests = () => { 승인 대기 의뢰 총 {data && data.length > 0 ? data.length : 0}건 - + {sortByRecent ? '최신순' : '오래된 순'} @@ -131,7 +131,9 @@ const LoadingWrapper = styled.div` font-size: 17px; `; -const SortWrapper = styled.button<{ rotate: boolean }>` +const SortWrapper = styled.button.withConfig({ + shouldForwardProp: (prop) => prop !== 'rotate', +})<{ rotate: boolean }>` border-style: none; background: inherit; display: flex; diff --git a/src/components/PromotionAdmin/Home/RequestSummary/WaitingRequestsList.tsx b/src/components/PromotionAdmin/Home/RequestSummary/WaitingRequestsList.tsx index 8c32f40..2391753 100644 --- a/src/components/PromotionAdmin/Home/RequestSummary/WaitingRequestsList.tsx +++ b/src/components/PromotionAdmin/Home/RequestSummary/WaitingRequestsList.tsx @@ -14,7 +14,16 @@ type Props = { hoverBackgroundColor: string; }; -const WaitingRequestsList = ({ requestId, organization, clientName, description, category, date, email, hoverBackgroundColor }: Props) => { +const WaitingRequestsList = ({ + requestId, + organization, + clientName, + description, + category, + date, + email, + hoverBackgroundColor, +}: Props) => { const limitedOrganization = organization.length > 10 ? organization.slice(0, 10) + '...' : organization; const limitedDescription = description.length > 28 ? description.slice(0, 28) + '...' : description; const limitedName = clientName.length > 7 ? clientName.slice(0, 7) + '...' : clientName; @@ -36,7 +45,9 @@ const WaitingRequestsList = ({ requestId, organization, clientName, description, export default WaitingRequestsList; -const Container = styled(Link)<{ hoverBackgroundColor: string }>` +const Container = styled(Link).withConfig({ + shouldForwardProp: (prop) => prop !== 'hoverBackgroundColor', +})<{ hoverBackgroundColor: string }>` display: flex; align-items: center; justify-content: space-between; @@ -67,6 +78,7 @@ const Container = styled(Link)<{ hoverBackgroundColor: string }>` width: 150px; } `; + const OrganizationWrapper = styled.div` width: 150px; white-space: nowrap; @@ -91,4 +103,4 @@ const DetailWrapper = styled.div` font-family: 'pretendard-regular'; font-size: 13px; } -`; \ No newline at end of file +`; diff --git a/src/hooks/useGraphData.tsx b/src/hooks/useGraphData.tsx index ff53ea4..fbc75e7 100644 --- a/src/hooks/useGraphData.tsx +++ b/src/hooks/useGraphData.tsx @@ -3,15 +3,21 @@ import { useState, useEffect, useCallback } from 'react'; import dayjs from 'dayjs'; // 해당 기간의 데이터를 가져오는 함수 -const fetchDataByRange = async (category: string, state: string, startDate: dayjs.Dayjs, endDate: dayjs.Dayjs, fetchFunction: Function) => { +const fetchDataByRange = async ( + category: string, + state: string, + startDate: dayjs.Dayjs, + endDate: dayjs.Dayjs, + fetchFunction: Function, +) => { if (!startDate || !endDate) return []; const startYear = startDate.year(); const startMonth = startDate.month() + 1; const endYear = endDate.year(); const endMonth = endDate.month() + 1; - console.log(category+" "+state) + try { - return await fetchFunction(startYear,startMonth,endYear,endMonth,category,state); + return await fetchFunction(startYear, startMonth, endYear, endMonth, category, state); } catch (error) { console.error(`[❌Error ${fetchFunction.name}]`, error); return []; @@ -35,10 +41,14 @@ const processChartData = (startDate: dayjs.Dayjs, endDate: dayjs.Dayjs, data: an const foundData = data.find( (item: { year: number; month: number }) => item.year === month.year && item.month === month.month, ); - const count = foundData ? (division === 'statistics' ? foundData.views - : (Object.values(foundData.RequestCount)as number[]) - .reduce((acc: number, value: number)=>{return acc+value},0)) : 0; - console.log(count) + const count = foundData + ? division === 'statistics' + ? foundData.views + : (Object.values(foundData.RequestCount) as number[]).reduce((acc: number, value: number) => { + return acc + value; + }, 0) + : 0; + return { x: `${month.year}년 ${month.month}월`, y: count, @@ -53,8 +63,8 @@ const useGraphData = ( defaultEndDate: dayjs.Dayjs, division: 'statistics' | 'request', ) => { - const [category, setCategory] = useState(division==='request'?'all':'ALL'); // 카테고리 상태 추가 - const [state, setState] = useState(division==='request'?'all':'ALL'); // 카테고리 상태 추가 + const [category, setCategory] = useState(division === 'request' ? 'all' : 'ALL'); // 카테고리 상태 추가 + const [state, setState] = useState(division === 'request' ? 'all' : 'ALL'); // 카테고리 상태 추가 const [startDate, setStartDate] = useState(defaultStartDate); const [endDate, setEndDate] = useState(defaultEndDate); const [data, setData] = useState([]); @@ -63,7 +73,13 @@ const useGraphData = ( const fetchData = useCallback(async () => { setLoading(true); - const fetchedData = await fetchDataByRange(category, state, startDate || dayjs(), endDate || dayjs(), fetchFunction); + const fetchedData = await fetchDataByRange( + category, + state, + startDate || dayjs(), + endDate || dayjs(), + fetchFunction, + ); setData(fetchedData); setProcessedData(processChartData(startDate || dayjs(), endDate || dayjs(), fetchedData, division)); setLoading(false); @@ -74,14 +90,14 @@ const useGraphData = ( }, [category, state, startDate, endDate, division, fetchData]); //카테고리 변경 핸들러 - const handleCategoryChange=(category:string)=>{ + const handleCategoryChange = (category: string) => { setCategory(category); - setState(division==='request'?'all':'ALL'); - } + setState(division === 'request' ? 'all' : 'ALL'); + }; //상태 변경 핸들러 - const handleStateChange=(state:string)=>{ + const handleStateChange = (state: string) => { setState(state); - } + }; // 시작일 변경 핸들러 const handleStartDateChange = (newStartDate: dayjs.Dayjs | null) => { @@ -94,8 +110,20 @@ const useGraphData = ( }; // 상태와 핸들러를 반환 - return { category, state, startDate, endDate, data, processedData, loading, - handleCategoryChange,handleStateChange,handleStartDateChange, handleEndDateChange, division }; + return { + category, + state, + startDate, + endDate, + data, + processedData, + loading, + handleCategoryChange, + handleStateChange, + handleStartDateChange, + handleEndDateChange, + division, + }; }; export default useGraphData;