///
Search
👋

custom Profile 개발기

태그
Project
클라썸

Situation

사용자들이 프로필 화면에서 자신을 표현하는 다양한 요소들을 추가할 수 있도록 하는 기능을 개발해야했습니다..
쉽게 얘기해서 노션에서 사용하는 멀티 태그 같은 것들을 개발한다고 보면 됩니다.
예를 들어, 직장인이라고 하면 (이름, 사원번호, 직급, 부서, 입사일시) 등이 될 수 있고 학생이라면 (이름, 학번, 학과, 입학일시, 거주 기숙사) 등이 될 수 있을 것입니다.
각각의 요소를 필드라고 하며, 해당 필드가 가질 수 있는 값을 태그라고 칭하겠습니다.
필드에는 여러 타입이 있습니다. (boolean, string, number, 단일선택, 다중 선택, 일시 ) 등을 가질 수 있습니다.
각 필드를 지정할 수 있는 사람은 관리자만 지정할 수 있습니다.

Task

이런 커스텀 프로필 기능은 다음과 같은 요구사항을 만족해야합니다.
1.
조직 관리자가 중앙 집중형으로 필드를 관리할 수 있습니다.
a.
조직 관리자가 필드를 생성/수정/삭제 시 해당 조직원들의 필드는 모두 생성/수정/삭제가 됩니다.
2.
조직 관리자의 조직원 대시보드에 컬럼으로 그 값을 볼 수 있습니다.
a.
해당 값으로 필터링,ordering 등이 가능해야합니다.
b.
노션의 데이터베이스가 그 예시입니다.

Action

ERD

우선 ERD는 다음과 같이 구성하였습니다.
erDiagram
	CustomField ||--o{ CustomFieldTag: relation
	CustomFieldTag ||--o{ CustomFieldToMember: relation
	InstituteMember ||--o| CustomFieldToMember: relation
	CustomField {
		id primary
		name string
		dataType Enum
		selectType Enum
		settingType Enum
	} 
	CustomFieldTag {
		id primary
		value string
	}
	
	CustomFieldToMember {
		instituteMemberId foreign
		fieldTagId foreign
		unique instituteMemberIdFieldTagId
	}
Mermaid
복사
customField
커스텀필드를 담는 테이블입니다. 각 조직에서 사용하고 싶은 속성이 그 예시가 됩니다.
이를 위한 dataType과 selectType이 Enum으로 관리됩니다.
각 조직 내 공간에 붙는 필드인지, 멤버에 붙는 필드인지에 따라 settingType으로 관리할 수 있습니다.
cusotmFieldTag
필드의 선택지를 담는 테이블입니다.
이 때 선택지가 아닌 타입의 경우, 사실상 선택지의 개수 제한이 없는 타입으로 간주합니다.
예를 들어 숫자 타입의 경우 - 정수만 생각하겠습니다. - 1,2,3,4 라는 값으로 볼 수 있을텐데, 이 경우 1,2,3,4 라는 customFieldTag가 생겨나게 됩니다.
즉 한 사람이 특정 값을 입력하면 이에 맞는 선택지가 새로 생겨나는 식입니다.
이는 선택지가 있는 경우와 데이터 모델을 통일성 있게 맞추기 위함입니다.
customFieldToMember
어떤 사람이 어떤 선택지를 선택했는지를 담는 테이블입니다. 선택하지 않았다면 row가 존재하지 않습니다.

구현

구현에서 특히 신경을 썼던 부분은 순서와 관한 부분입니다. 여러 조직 관리자가 동시에 customField의 order를 설정할 수 있는 상황이었으므로 order가 항상 다른 값을 가질 것임을 보장할 수 없었습니다. 따라서 순서를 update하는 경우, pessimistic_write로 순서를 보장합니다.
이 때 해당 트랜잭션이 시작할 때 pessimistic_write를 걸어, 동시에 일어날 수 있는 수정 트랜잭션의 읽기 또한 막습니다.
public async saveCustomField<T extends SettingType>( instituteId: number, body: ICustomField.MutateListDto, type: T, qr?: QueryRunner, ): Promise<CustomField[]> { const appended = body.appended const deleted = body.deleted const updated = body.updated const needToUpdateInstituteMemberList = [] let manager: EntityManager let queryRunner: QueryRunner if (qr) { queryRunner = qr manager = queryRunner.manager } else { queryRunner = this.connection.createQueryRunner() manager = queryRunner.manager } try { await queryRunner.connect() await queryRunner.startTransaction() const instituteRepo = queryRunner.manager.getCustomRepository(InstituteRepository) const instituteMemberRepo = queryRunner.manager.getCustomRepository( InstituteMemberRepository, ) const customFieldRepo = queryRunner.manager.getCustomRepository( CustomFieldRepository, ) const institute = await instituteRepo.findOne({ id: instituteId }) /** * 0. validation * dto를 순회하면서 validator로 검사합니다. */ const validator = new CustomFieldMutateDtoValidator() ... ... /** * 삭제 -> 생성 -> 업데이트 순서로 진행 * 해당 save 프로세스 진행 전 pessimistic_write를 설정함. * mysql 특성 상 phantom read 발생하지 않음 */ const customFieldsBeforeDelete = await customFieldRepo.find({ where: { instituteId: instituteId, settingType: type, }, lock: { mode: 'pessimistic_write', }, })
TypeScript
복사

Result

기본 프로필이긴 하지만 위와 같은 프로필에서 항목을 계속 추가할 수 있습니다.