Giter VIP home page Giter VIP logo

street-drop-server's Introduction

Street Drop - 슀트릿 λ“œλž

Hits codecov Github Action

πŸ“š Quick Link

πŸ’β€β™‚οΈ Introduction

intro-main

intro-description home let's-go

πŸ’β€β™€οΈ Documents

🚎 Architecture

server-architecture

  • λΉ„μš© μ ˆκ°μ„ μœ„ν•΄μ„œ Test(Dev), Admin μ„œλ²„λŠ” ν™ˆμ„œλ²„λ₯Ό ν†΅ν•΄μ„œ μš΄μ˜ν•˜κ³  있으며, Prod μ„œλ²„λŠ” μ„œλΉ„μŠ€μ˜ μ•ˆμ •μ„±μ„ μœ„ν•΄μ„œ AWS EC2λ₯Ό μ‚¬μš©ν•˜μ—¬ μš΄μ˜ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
  • λ„€νŠΈμ›Œν¬ IOκ°€ 주된 μž‘μ—…μ΄κ³ , μ™ΈλΆ€ API만 μ—°λ™λ˜κ³  데이터 λ² μ΄μŠ€μ— μ˜μ‘΄μ„±μ΄ μ—†λŠ” 검색 μ„œλ²„λŠ” λ³„λ„λ‘œ λΆ„λ¦¬ν•˜μ—¬ κ΅¬μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • μœ μ € 레벨 μ—…λ°μ΄νŠΈ, μ˜ˆμ•½ ν‘Έμ‹œ λ°œμ†‘λ“±μ„ μœ„ν•˜μ—¬, λ°°μΉ˜μ„œλ²„, μ•Œλ¦Ό μ„œλ²„λ₯Ό λΆ„λ¦¬ν•˜μ—¬ κ΅¬μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ—„οΈ Directory Structure and Multi Module

πŸ“‚ Directory Structure

β”œβ”€β”€ .github
β”œβ”€β”€ backend
β”‚   β”œβ”€β”€ streetdrop-admin  # κ΄€λ¦¬μž μ›Ή μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜
β”‚   β”‚   β”œβ”€β”€ streetdrop-admin-server  # κ΄€λ¦¬μž μ›Ή API μ„œλ²„
β”‚   β”‚   β”œβ”€β”€ streetdrop-admin-web # κ΄€λ¦¬μž μ›Ή ν”„λ‘ νŠΈμ—”λ“œ
β”‚   β”‚   └── streetdrop-admin-web-server # κ΄€λ¦¬μž μ›Ή ν”„λ‘ νŠΈμ—”λ“œ 정적 배포용 μ„œλ²„
β”‚   β”œβ”€β”€ streetdrop-api  # API μ„œλ²„
β”‚   β”œβ”€β”€ streetdrop-batch  # 배치 μ„œλ²„
β”‚   β”œβ”€β”€ streetdrop-common  # 곡톡 λͺ¨λ“ˆ
β”‚   β”œβ”€β”€ streetdrop-domain  # 도메인 λͺ¨λ“ˆ
β”‚   β”œβ”€β”€ streetdrop-notification  # μ•Œλ¦Ό μ„œλ²„
β”‚   └── streetdrop-search  # 검색 μ„œλ²„
β”œβ”€β”€ docs # λ¬Έμ„œκ΄€λ¦¬μš© 폴더
└── infra # 인프라 κ΄€λ¦¬μš© 폴더 - Grafana, Prometheus, Jenkins
  • μ–΄λ“œλ―Όμ˜ 경우 ν”„λ‘ νŠΈ μ—”λ“œλ₯Ό Spring Boot둜 정적 배포할 경우, λΉŒλ“œ μ‹œκ°„μ΄ 였래걸렀 Node.js둜 μ •μ νŒŒμΌμ„ λ°°ν¬ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

🧩 Multi Module

multi-module

  • λ©€ν‹° λͺ¨λ“ˆμ„ μ μš©ν•˜μ—¬ 역할에 따라 λͺ¨λ“ˆμ„ λΆ„λ¦¬ν•˜μ˜€μŠ΅λ‹ˆλ‹€.
  • Common λͺ¨λ“ˆμ€ Validationλ“±μ˜ 순수 μžλ°” μ½”λ“œ, Domain λͺ¨λ“ˆμ€ μ—”ν‹°ν‹° 정보λ₯Ό λ‹΄κ³  μžˆλŠ” λͺ¨λ“ˆλ‘œ κ΅¬μ„±λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.
  • 각 API, 배치, μ•Œλ¦Ό, 검색 μ„œλ²„λŠ” Domain λͺ¨λ“ˆμ„ μ˜μ‘΄μ„±μœΌλ‘œ 가지고 있으며, λͺ¨λ“ˆκ°„μ˜ μ˜μ‘΄μ„±μ€ μƒμœ„ λͺ¨λ“ˆμ΄ ν•˜μœ„ λͺ¨λ“ˆλ§Œμ„ μ˜μ‘΄ν•˜λ„λ‘ κ΅¬μ„±ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

πŸ“ˆ Dependency and Quick Start

πŸ“¦ Dependency

  • 기본적인 μ˜μ‘΄μ„±μž…λ‹ˆλ‹€. μžμ„Έν•œ μ˜μ‘΄μ„±μ€ 각 λͺ¨λ“ˆλ³„ build.gradle 파일과 λ¬Έμ„œλ₯Ό μ°Έκ³ ν•΄μ£Όμ„Έμš”.
    • Java 19
    • Gradle 7.6.1
    • MySQL 8.0.33
    • Spring Boot 3.0.6

πŸš€ Quick Start

  • λͺ¨λ“ˆ λ³„λ‘œ λΉŒλ“œν•˜κΈ° μœ„ν•΄μ„œλŠ” backend λ””λ ‰ν† λ¦¬μ—μ„œ ./gradlew :{λͺ¨λ“ˆλͺ…}:build λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
  • 예λ₯Ό λ“€μ–΄, streetdrop-api λͺ¨λ“ˆμ„ λΉŒλ“œν•˜κΈ° μœ„ν•΄μ„œλŠ” backend λ””λ ‰ν† λ¦¬μ—μ„œ ./gradlew streetdrop-api:build λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€.
  • ν”„λ‘œνŒŒμΌμ€ dev, prod, local둜 κ΅¬μ„±λ˜μ–΄ 있으며, 각 ν™˜κ²½λ³„λ‘œ Swagger 지원, API ν…ŒμŠ€νŠΈμš© 헀더등이 λ‹€λ₯΄κ²Œ κ΅¬μ„±λ˜μ–΄ μžˆμœΌλ―€λ‘œ μ μ ˆν•œ ν”„λ‘œνŒŒμΌμ„ μ„ νƒν•΄μ„œ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

πŸ–₯️ Tech Stack

Framework -

ORM -

Authorization -

Test -

Database -

AWS -

Monitoring -

Admin Web Page -

Other -

πŸ“ˆ DataBase Schema

MySQL Schema

ERD

πŸ‘₯ Contributors

πŸ‘œ Repository

πŸ§‘β€πŸ’» Server Engineers

YunYoung Seonghun Siyeon






street-drop-server's People

Contributors

seonghun-dev avatar siyeonson avatar yunyoung1819 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

street-drop-server's Issues

Insert sample data

DESCRIPTION

add insert_sample_data.sql

TODO

  • insert sample data to test server

TEST

no testing required

Add custom header idfv resolver

DESCRIPTION

Header에 λ‹΄κΈ΄ idfvκ°’ 받아와 μœ μ € 생성 및 정보 κ°€μ Έμ˜€λŠ” 인터셉터 개발

TEST

Refactor to multi modules

DESCRIPTION

Refactor to multi modules

TODO

structure

  • backend
    • search
    • batch
    • api
    • core
    • admin
      • web
      • server
  • infra

TEST

no need to test

ResponseDTO Setting

DESCRIPTION

ResponseDTO Setting

TODO

[] ResponseDTO Setting

TEST

  • Don't need test - setup

Add Get Near Item Points API

DESCRIPTION

μ£Όλ³€μ˜ μ•„μ΄ν…œλ“€μ˜ μ’Œν‘œ 쑰회 - poi μš©λ„

TEST

    @DisplayName("[GET] λ‚΄ μ£Όλ³€ λ“œλž μ•„μ΄ν…œ poi 쑰회")
    @Nested
    class GetNearItemPointsTest {
        @Nested
        @DisplayName("지역별 λ“œλž μ•„μ΄ν…œ 개수 쑰회 성곡")
        class Success {
            double latitude;
            double longitude;

            @BeforeEach
            void setUp() {
                latitude = 37.123456;
                longitude = 127.123456;
            }

            @DisplayName("지역별 λ“œλž μ•„μ΄ν…œ 개수 쑰회 성곡 - 2개 쑰회 성곡, 거리 쑰회 X")
            @Test
            void getNearItemPointsTest() throws Exception {
                PoiResponseDto.PoiDto poiResponseDto1 = new PoiResponseDto.PoiDto(1L, "/butter1.jpg", 37.123454, 127.123456);
                PoiResponseDto.PoiDto poiResponseDto2 = new PoiResponseDto.PoiDto(2L, "/karina2.jpg", 37.123436, 127.123466);
                PoiResponseDto poiResponseDto = new PoiResponseDto(List.of(poiResponseDto1, poiResponseDto2));

                given(itemService.getNearItemPoints(any(NearItemRequestDto.class))).willReturn(poiResponseDto);

                var response = mvc.perform(
                        get("/items")
                                .param("latitude", String.valueOf(latitude))
                                .param("longitude", String.valueOf(longitude))
                );

                response.andExpect(status().isOk())
                        .andExpect(jsonPath("$.poi").isArray())
                        .andExpect(jsonPath("$.poi[0].itemId").value(1L))
                        .andExpect(jsonPath("$.poi[0].albumCover").value("/butter1.jpg"))
                        .andExpect(jsonPath("$.poi[0].latitude").value(37.123454))
                        .andExpect(jsonPath("$.poi[0].longitude").value(127.123456))
                        .andExpect(jsonPath("$.poi[1].itemId").value(2L))
                        .andExpect(jsonPath("$.poi[1].albumCover").value("/karina2.jpg"))
                        .andExpect(jsonPath("$.poi[1].latitude").value(37.123436))
                        .andExpect(jsonPath("$.poi[1].longitude").value(127.123466));

            }

            @DisplayName("지역별 λ“œλž μ•„μ΄ν…œ 개수 쑰회 성곡 - 0개 쑰회 성곡")
            @Test
            void getNearItemPointsTest2() throws Exception {
                PoiResponseDto poiResponseDto = new PoiResponseDto(List.of());
                given(itemService.getNearItemPoints(any(NearItemRequestDto.class))).willReturn(poiResponseDto);

                var response = mvc.perform(
                        get("/items")
                                .param("latitude", String.valueOf(latitude))
                                .param("longitude", String.valueOf(longitude))
                );

                response.andExpect(status().isOk())
                        .andExpect(jsonPath("$.poi").isArray())
                        .andExpect(jsonPath("$.poi").isEmpty());

            }

            @DisplayName("지역별 λ“œλž μ•„μ΄ν…œ 개수 쑰회 성곡 - 1개 쑰회 성곡, λ²”μœ„ 지정")
            @Test
            void getNearItemPointsTest3() throws Exception {
                PoiResponseDto.PoiDto poiResponseDto1 = new PoiResponseDto.PoiDto(1L, "/butter1.jpg", 37.123454, 127.123456);
                PoiResponseDto poiResponseDto = new PoiResponseDto(List.of(poiResponseDto1));

                given(itemService.getNearItemPoints(any(NearItemRequestDto.class))).willReturn(poiResponseDto);

                var response = mvc.perform(
                        get("/items")
                                .param("latitude", String.valueOf(latitude))
                                .param("longitude", String.valueOf(longitude))
                                .param("distance", String.valueOf(1000))
                );

                response.andExpect(status().isOk())
                        .andExpect(jsonPath("$.poi").isArray())
                        .andExpect(jsonPath("$.poi[0].itemId").value(1L))
                        .andExpect(jsonPath("$.poi[0].albumCover").value("/butter1.jpg"))
                        .andExpect(jsonPath("$.poi[0].latitude").value(37.123454))
                        .andExpect(jsonPath("$.poi[0].longitude").value(127.123456));

            }

        }

        @Nested
        @DisplayName("지역별 λ“œλž μ•„μ΄ν…œ 개수 쑰회 μ‹€νŒ¨")
        class Fail {

            @DisplayName("Latitude μœ νš¨μ„± 검사 μ‹€νŒ¨ - 값이 μ—†λŠ” 경우")
            @Test
            void getNearItemPointsFail1() throws Exception {

                NearItemRequestDto nearItemRequestDto = new NearItemRequestDto();
                nearItemRequestDto.setLongitude(127.123456);

                var response = mvc.perform(
                        get("/items")
                                .param("longitude", String.valueOf(nearItemRequestDto.getLongitude()))
                );

                response.andExpect(status().isBadRequest())
                        .andExpect(jsonPath("$.message").value("Latitude is required"));

            }

            @DisplayName("Latitude μœ νš¨μ„± 검사 μ‹€νŒ¨ - λ²”μœ„μ— λ§žμ§€ μ•ŠλŠ” 경우")
            @Test
            void getNearItemPointsFail2() throws Exception {

                NearItemRequestDto nearItemRequestDto = new NearItemRequestDto();
                nearItemRequestDto.setLatitude(1000.0);
                nearItemRequestDto.setLongitude(127.123456);


                var response = mvc.perform(
                        get("/items")
                                .param("latitude", String.valueOf(nearItemRequestDto.getLatitude()))
                                .param("longitude", String.valueOf(nearItemRequestDto.getLongitude()))
                );

                response.andExpect(status().isBadRequest())
                        .andExpect(jsonPath("$.message").value("Not valid latitude, must be between -90 and 90"));

            }

            @DisplayName("Longitude μœ νš¨μ„± 검사 μ‹€νŒ¨  - 값이 μ—†λŠ” 경우")
            @Test
            void getNearItemPointsFail3() throws Exception {

                NearItemRequestDto nearItemRequestDto = new NearItemRequestDto();
                nearItemRequestDto.setLatitude(37.123456);

                var response = mvc.perform(
                        get("/items")
                                .param("latitude", String.valueOf(nearItemRequestDto.getLatitude()))
                );

                response.andExpect(status().isBadRequest())
                        .andExpect(jsonPath("$.message").value("Longitude is required"));

            }

            @DisplayName("Longitude μœ νš¨μ„± 검사 μ‹€νŒ¨ - λ²”μœ„μ— λ§žμ§€ μ•ŠλŠ” 경우")
            @Test
            void getNearItemPointsFail4() throws Exception {

                NearItemRequestDto nearItemRequestDto = new NearItemRequestDto();
                nearItemRequestDto.setLatitude(37.123456);
                nearItemRequestDto.setLongitude(1000.0);

                var response = mvc.perform(
                        get("/items")
                                .param("latitude", String.valueOf(nearItemRequestDto.getLatitude()))
                                .param("longitude", String.valueOf(nearItemRequestDto.getLongitude()))
                );

                response.andExpect(status().isBadRequest())
                        .andExpect(jsonPath("$.message").value("Not valid longitude, must be between -180 and 180"));

            }
        }
    }

Add Get drop item counts by region API

DESCRIPTION

  • 동별 λ“œλžλœ μŒμ•… 개수 API

TODO

  • Entity μΆ”κ°€
  • 동별 λ“œλžλœ μŒμ•… 개수 쑰회 API

TEST

The Following shoud be passed

@DisplayName("지역별 λ“œλž μ•„μ΄ν…œ 개수 쑰회")
@Test
void VillageItemCountTest() throws Exception {
    var VillageName = "μ’…λ‘œκ΅¬ 사직동";
    var villageItemCountResponseDto = new VillageItemCountResponseDto(1);
    
    given(villageItemService.count(VillageName)).willReturn(villageItemCountResponseDto);
    
    var response = mvc.perform(
            get("/village-items/count")
                    .param("villageName", VillageName)
    );

    response.andExpect(status().isOk())
            .andExpect(jsonPath("$.numberOfDroppedMusic").value(1));
}

Swagger Setting

DESCRIPTION

  • Swagger Setting

TODO

  • Add Swagger to gradle
  • Basic Swagger setting

TEST

  • ν…ŒμŠ€νŠΈ λΆˆν•„μš”(μ„ΈνŒ…)

Add Admin User API

DESCRIPTION

Admin User κ΄€λ ¨ API 개발

TODO

  • admin server μœ μ € 전체 쑰회 api κ΅¬ν˜„
  • admin web μœ μ € 쑰회 νŽ˜μ΄μ§€ κ΅¬ν˜„
  • admin web rest api 연동 (admin server 배포 ν›„ 배포 url둜 λ³€κ²½ ν•„μš”)

TEST

Adminα„Œα…₯α†«α„Žα…¦α„‰α…‘α„‹α…­α†Όα„Œα…‘α„Œα…©α„’α…¬

Add Entity Class

DESCRIPTION

μ—”ν‹°ν‹° 클래슀λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

TODO

TEST

Add Music Genre Save

DESCRIPTION

μ•„μ΄ν…œ λ“œλžμ‹œ μž₯λ₯΄λ₯Ό μ €μž₯ν•˜λ„λ‘ μΆ”κ°€

Change response structure in Item Detail API

DESCRIPTION

NEED TO FIX

{
    [
        {
            "itemId": 1,
            "user": {
                "nickname" : "혜린",
                "profileImage" : "λ§ν¬ν˜•μ‹",
                "musicApp" : "youtubemusic"
            },
            "location": {
                "address": "성동ꡬ μ„±μˆ˜1κ°€ 1동" 
            },
            "music": {
                "title": "ν•˜μž…λ³΄μ΄",
                "artist": "λ‰΄μ§„μŠ€",
                "albumImage": "λ§ν¬ν˜•μ‹"
                "genre": [
                    "Rock",
                    "K-pop"
                ]
            },
            "content": "λ‚¨μΉœμ΄λž‘ μΉ΄λ¦¬λ‚˜ μ–˜κΈ°ν•˜λ‹€κ°€ μ‹Έμš΄ 썰 ν‘Όλ‹€",
            "createdAt": "yyyy-MM-dd HH:mm:ss:SSS"
        }
    ]
}

RESULT

{
  "items": [
        {
            "itemId": 1,
            "user": {
                "nickname" : "혜린",
                "profileImage" : "λ§ν¬ν˜•μ‹",
                "musicApp" : "youtubemusic"
            },
            "location": {
                "address": "성동ꡬ μ„±μˆ˜1κ°€ 1동" 
            },
            "music": {
                "title": "ν•˜μž…λ³΄μ΄",
                "artist": "λ‰΄μ§„μŠ€",
                "albumImage": "λ§ν¬ν˜•μ‹"
                "genre": [
                    "Rock",
                    "K-pop"
                ]
            },
            "content": "λ‚¨μΉœμ΄λž‘ μΉ΄λ¦¬λ‚˜ μ–˜κΈ°ν•˜λ‹€κ°€ μ‹Έμš΄ 썰 ν‘Όλ‹€",
            "createdAt": "yyyy-MM-dd HH:mm:ss:SSS"
        }
    ]
}

TODO

  • wrap with "items"

TEST

  • need to fix some test code

Refactorring Item Drop API

DESCRIPTION

μ•„μ΄ν…œ 등둝 APIμ—μ„œ ItemRequestDTO에 λŒ€ν•œ λ¦¬νŒ©ν† λ§ 진행

TODO

TEST

Items API Pagination

DESCRIPTION

Handling pagination for Items POI retrieval API(#35) and Items detail retrieval API(#31)

INTRODUCTION

Advantages of pagination is to prevent slow API response due to returning a large amount of data at once.

PROBLEM

In the project, an infinite scroll (front and back) was implemented.
Raises the question of when to make additional calls beyond the [.. , 19, 20, 1, 2, 3, ...] format, such as [21, 22, ..].

CONVERSATION

  • Whether to implement pagination or not.
  • How to implement it.

DISCUSSION

  • 2023.05.20 : Return the data as a List without implementing pagination

CD setup using Github Action

DESCRIPTION

Github Action, Docker, DockerHub Using Street Drop API CD Set up

  1. Add api-deploy.yml, Dockerfile
  2. Seperated application.yml (dev, prod, mock)
  3. GitHub Secrets Variable, DockerHub Setting
  4. AWS EC2 install docker, docker-compose

TEST

No need Test, Setup

Drop Item Refactoring

DESCRIPTION

λ„λ©”μΈκ°„μ˜ μ˜μ‘΄μ„± 제거

TODO

TEST

Add Test Code for Item Detail API

DESCRIPTION

Add Test Code for Item Detail API

TODO

  • ItemService test code
  • ItemController test code

TEST

  • FindNearItems not exist
  • FindNearItems success
  • When invalid values ​​are entered

Add Item likes API

DESCRIPTION

1μ°¨ MVP에 있던 μ’‹μ•„μš” κΈ°λŠ₯ κ΅¬ν˜„

TODO

TEST

add search api

DESCRIPTION

add search api for cache and Auto-renew access tokens

Add genre entity

DESCRIPTION

Add genre entity

TODO

  • song table and genre table are M:N relationship
  • song_genre table is join table for song table and genre table

TEST

no testing required

Move security in core module to api module

DESCRIPTION

μ‹œνλ¦¬ν‹° κ΄€λ ¨νŒŒμΌ coreμ—μ„œ api λͺ¨λ“ˆλ‘œ 이동
adminμͺ½μ—μ„œλŠ” λ‹€λ₯Έ μ‹œνλ¦¬ν‹° μ„€μ • μ‚¬μš©

Add MongoDB appender to logback

DESCRIPTION

MongoDB 둜그 ν™œμš©μ„ μœ„ν•œ logback에 mongodb appender μΆ”κ°€

TEST

No need Test

Automation Test Setup using Jacoco and Codecov

DESCRIPTION

Jacoco와 Codecovλ₯Ό μ΄μš©ν•œ ν…ŒμŠ€νŠΈ μžλ™ν™”(CI)

TODO

  • Jacoco와 Codecovλ₯Ό μ΄μš©ν•œ ν…ŒμŠ€νŠΈ μžλ™ν™”(CI)

TEST

Don't need test - setup

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.